diff --git a/asset/views.py b/asset/views.py index 861436ce0..bada465d2 100644 --- a/asset/views.py +++ b/asset/views.py @@ -52,6 +52,7 @@ from asset.models import ( ) from base.methods import ( closest_numbers, + eval_validate, filtersubordinates, get_key_instances, get_pagination, @@ -301,7 +302,7 @@ def asset_delete(request, asset_id): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) instances_ids = request.GET.get("requests_ids", "[]") - instances_list = eval(instances_ids) + instances_list = eval_validate(instances_ids) if status == "In use": messages.info(request, _("Asset is in use")) return redirect( @@ -314,7 +315,7 @@ def asset_delete(request, asset_id): ) else: asset_del(request, asset) - if len(eval(instances_ids)) <= 1: + if len(eval_validate(instances_ids)) <= 1: return HttpResponse("") if Asset.find(asset.id): @@ -533,6 +534,38 @@ def asset_category_view_search_filter(request): return render(request, "category/asset_category.html", context) +def request_creation_hx_returns(referer, user): + """ + Determines the hx_url and hx_target based on the referer path + for asset request creation + """ + referer = "/" + "/".join(referer.split("/")[3:]) + # Map referer paths to corresponding URLs and targets + hx_map = { + "/": ("asset-dashboard-requests", "dashboardAssetRequests"), + "/asset/dashboard/": ("asset-dashboard-requests", "dashboardAssetRequests"), + "/asset/asset-request-allocation-view/": ( + "asset-request-allocation-view-search-filter", + "asset_request_allocation_list", + ), + "/employee/employee-profile/": ( + "profile-asset-tab", + "asset_target", + ), + } + + hx_url, hx_target = hx_map.get( + referer, (None, None) + ) # Default to None if not in map + + if hx_url == "profile-asset-tab": + hx_url = reverse(hx_url, kwargs={"emp_id": user.employee_get.id}) + else: + hx_url = reverse(hx_url) if hx_url else None + + return hx_url, hx_target + + @login_required def asset_request_creation(request): """ @@ -547,14 +580,16 @@ def asset_request_creation(request): messages displayed. """ # intitial = {'requested_employee_id':request.user.employee_get} + + referer = request.META.get("HTTP_REFERER", "/") + hx_url, hx_target = request_creation_hx_returns(referer, request.user) form = AssetRequestForm(user=request.user) - context = {"asset_request_form": form} + context = {"asset_request_form": form, "hx_url": hx_url, "hx_target": hx_target} if request.method == "POST": form = AssetRequestForm(request.POST, user=request.user) if form.is_valid(): form.save() messages.success(request, _("Asset request created!")) - return HttpResponse("") context["asset_request_form"] = form return render(request, "request_allocation/asset_request_creation.html", context) diff --git a/attendance/views/requests.py b/attendance/views/requests.py index 698f101bf..fda63f42d 100644 --- a/attendance/views/requests.py +++ b/attendance/views/requests.py @@ -33,6 +33,7 @@ from attendance.views.clock_in_out import early_out, late_come from base.methods import ( choosesubordinates, closest_numbers, + eval_validate, filtersubordinates, get_key_instances, is_reportingmanager, @@ -131,7 +132,7 @@ def request_new(request): This method is used to create new attendance requests """ - if request.GET.get("bulk") and eval(request.GET.get("bulk")): + if request.GET.get("bulk") and eval_validate(request.GET.get("bulk")): employee = request.user.employee_get form = BulkAttendanceRequestForm(initial={"employee_id": employee}) if request.method == "POST": @@ -758,7 +759,7 @@ def get_employee_shift(request): employee = Employee.objects.get(id=employee_id) shift = employee.get_shift form = NewRequestForm() - if request.GET.get("bulk") and eval(request.GET.get("bulk")): + if request.GET.get("bulk") and eval_validate(request.GET.get("bulk")): form = BulkAttendanceRequestForm() form.fields["shift_id"].queryset = EmployeeShift.objects.all() form.fields["shift_id"].widget.attrs["hx-trigger"] = "load,change" diff --git a/attendance/views/views.py b/attendance/views/views.py index 0b6560251..1852dc6a2 100644 --- a/attendance/views/views.py +++ b/attendance/views/views.py @@ -104,6 +104,7 @@ from base.forms import ( from base.methods import ( choosesubordinates, closest_numbers, + eval_validate, export_data, filtersubordinates, get_key_instances, @@ -1902,7 +1903,7 @@ def create_grace_time(request): Returns: GET : return grace time form template """ - is_default = eval(request.GET.get("default")) + is_default = eval_validate(request.GET.get("default")) form = GraceTimeForm(initial={"is_default": is_default}) if request.method == "POST": form = GraceTimeForm(request.POST) @@ -2694,7 +2695,7 @@ def delete_allowed_ips(request): allowed_ips = AttendanceAllowedIP.objects.first() ips = allowed_ips.additional_data["allowed_ips"] for id in ids: - ips.pop(eval(id)) + ips.pop(eval_validate(id)) allowed_ips.additional_data["allowed_ips"] = ips allowed_ips.save() diff --git a/base/methods.py b/base/methods.py index 22c09cddf..6019641b7 100644 --- a/base/methods.py +++ b/base/methods.py @@ -1,3 +1,4 @@ +import ast import calendar import json import os @@ -866,3 +867,11 @@ def format_date(date_str): except ValueError: continue raise ValueError(f"Invalid date format: {date_str}") + + +def eval_validate(value): + """ + Method to validate the dynamic value + """ + value = ast.literal_eval(value) + return value diff --git a/employee/admin.py b/employee/admin.py index 9b68e86f5..4a4202f85 100644 --- a/employee/admin.py +++ b/employee/admin.py @@ -22,8 +22,32 @@ from employee.models import ( # Register your models here. -admin.site.register(Employee) +# admin.site.register(Employee) admin.site.register(EmployeeBankDetails) admin.site.register(EmployeeWorkInformation, SimpleHistoryAdmin) admin.site.register([EmployeeNote, EmployeeTag, PolicyMultipleFile, Policy, BonusPoint]) admin.site.register([DisciplinaryAction, Actiontype]) + + +from django.contrib import admin + + +class MyModelAdmin(admin.ModelAdmin): + def delete_view(self, request, object_id, extra_context=None): + # Add custom context for the delete confirmation page + extra_context = extra_context or {} + extra_context["custom_message"] = ( + "Are you sure you want to delete this item? This action cannot be undone." + ) + # Call the superclass's delete_view to render the page + return super().delete_view(request, object_id, extra_context=extra_context) + + def get_deleted_objects(self, objs, request): + response = super().get_deleted_objects(objs, request) + print("+++++++++++++++") + print(response) + print("+++++++++++++++") + return response + + +admin.site.register(Employee, MyModelAdmin) diff --git a/employee/forms.py b/employee/forms.py index d9b558de9..28d85eb56 100644 --- a/employee/forms.py +++ b/employee/forms.py @@ -34,7 +34,7 @@ from django.template.loader import render_to_string from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as trans -from base.methods import reload_queryset +from base.methods import eval_validate, reload_queryset from employee.models import ( Actiontype, BonusPoint, @@ -228,7 +228,7 @@ class EmployeeForm(ModelForm): item = item[total_zero_leads:] if isinstance(item, list): item = item[-1] - if not incremented and isinstance(eval(str(item)), int): + if not incremented and isinstance(eval_validate(str(item)), int): item = int(item) + 1 incremented = True if isinstance(item, int): diff --git a/employee/models.py b/employee/models.py index 6e3ca5fe3..26e73b675 100644 --- a/employee/models.py +++ b/employee/models.py @@ -568,7 +568,7 @@ class EmployeeWorkInformation(models.Model): ) reporting_manager_id = models.ForeignKey( Employee, - on_delete=models.DO_NOTHING, + on_delete=models.PROTECT, blank=True, null=True, related_name="reporting_manager", diff --git a/employee/policies.py b/employee/policies.py index 87c4747e6..6a1f0bbe7 100644 --- a/employee/policies.py +++ b/employee/policies.py @@ -15,7 +15,12 @@ from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ -from base.methods import filtersubordinates, get_key_instances, paginator_qry +from base.methods import ( + eval_validate, + filtersubordinates, + get_key_instances, + paginator_qry, +) from employee.filters import DisciplinaryActionFilter, PolicyFilter from employee.forms import DisciplinaryActionForm, PolicyForm from employee.models import ( @@ -53,7 +58,7 @@ def create_policy(request): """ instance_id = request.GET.get("instance_id") instance = None - if isinstance(eval(str(instance_id)), int): + if isinstance(eval_validate(str(instance_id)), int): instance = Policy.objects.filter(id=instance_id).first() form = PolicyForm(instance=instance) if request.method == "POST": diff --git a/horilla_api/api_methods/employee/methods.py b/horilla_api/api_methods/employee/methods.py index 10fdcdfa4..5c0400873 100644 --- a/horilla_api/api_methods/employee/methods.py +++ b/horilla_api/api_methods/employee/methods.py @@ -1,5 +1,6 @@ import re +from base.methods import eval_validate from base.models import * from employee.models import * @@ -41,7 +42,7 @@ def get_next_badge_id(): item = item[total_zero_leads:] if isinstance(item, list): item = item[-1] - if not incremented and isinstance(eval(str(item)), int): + if not incremented and isinstance(eval_validate(str(item)), int): item = int(item) + 1 incremented = True if isinstance(item, int): diff --git a/horilla_api/api_views/payroll/views.py b/horilla_api/api_views/payroll/views.py index 32a3e62f6..430b7459f 100644 --- a/horilla_api/api_views/payroll/views.py +++ b/horilla_api/api_views/payroll/views.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from base.backends import ConfiguredEmailBackend +from base.methods import eval_validate from payroll.filters import ( AllowanceFilter, ContractFilter, @@ -331,7 +332,11 @@ class ReimbusementApproveRejectView(APIView): def post(self, request, pk): status = request.data.get("status", None) amount = request.data.get("amount", None) - amount = eval(request.data.get("amount")) if request.data.get("amount") else 0 + amount = ( + eval_validate(request.data.get("amount")) + if request.data.get("amount") + else 0 + ) amount = max(0, amount) reimbursement = Reimbursement.objects.filter(id=pk) if amount: diff --git a/horilla_automations/models.py b/horilla_automations/models.py index 37bdbcebe..538cb4809 100644 --- a/horilla_automations/models.py +++ b/horilla_automations/models.py @@ -2,6 +2,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _trans +from base.methods import eval_validate from base.models import HorillaMailTemplate from horilla.models import HorillaModel from horilla_views.cbv_methods import render_template @@ -73,7 +74,7 @@ class MailAutomation(HorillaModel): method that returns the display value for `mail_to` field """ - mail_to = eval(self.mail_to) + mail_to = eval_validate(self.mail_to) mappings = [] for mapping in mail_to: mapping = mapping.split("__") diff --git a/horilla_automations/signals.py b/horilla_automations/signals.py index 22f40b00c..47b520192 100644 --- a/horilla_automations/signals.py +++ b/horilla_automations/signals.py @@ -333,7 +333,7 @@ def send_mail(request, automation, instance): mail sending method """ from base.backends import ConfiguredEmailBackend - from base.methods import generate_pdf + from base.methods import eval_validate, generate_pdf from horilla_automations.methods.methods import ( get_model_class, get_related_field_model, @@ -346,7 +346,7 @@ def send_mail(request, automation, instance): model_class = get_related_field_model(model_class, automation.mail_details) mail_to_instance = model_class.objects.filter(pk=pk).first() tos = [] - for mapping in eval(automation.mail_to): + for mapping in eval_validate(automation.mail_to): result = getattribute(mail_to_instance, mapping) if isinstance(result, list): tos = tos + result diff --git a/horilla_views/cbv_methods.py b/horilla_views/cbv_methods.py index bcc42a254..2fb4714d1 100644 --- a/horilla_views/cbv_methods.py +++ b/horilla_views/cbv_methods.py @@ -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 diff --git a/horilla_views/forms.py b/horilla_views/forms.py index 9d8897346..cb1ce2be7 100644 --- a/horilla_views/forms.py +++ b/horilla_views/forms.py @@ -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")) diff --git a/horilla_views/generic/cbv/views.py b/horilla_views/generic/cbv/views.py index e0cc25114..5de78923c 100644 --- a/horilla_views/generic/cbv/views.py +++ b/horilla_views/generic/cbv/views.py @@ -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 diff --git a/horilla_views/templates/generic/components.html b/horilla_views/templates/generic/components.html index fcc5b1a72..b58a4d4ab 100644 --- a/horilla_views/templates/generic/components.html +++ b/horilla_views/templates/generic/components.html @@ -1,3 +1,12 @@ + + + +
+ $("#reloadMessagesButton").click() + + +
+ {% trans "Delete Confirmation" %} + +
+ +
+
+ +
+
+
+ +
+
+ + +
+
+ {% trans "Deleting the record" %} '{{delete_object}}' {% trans "would require managing the following related objects:" %} +
+
+ {% trans "Protected Records" %} ({{protected|length}}) +
+
    + {% for summary in protected_objects_count.items %} +
  • {{summary.0|capfirst}} : {{summary.1|capfirst}}
  • + {% endfor %} +
+
+ {% trans "Other Related Records" %} ({{model_count_sum}}) +
+
    + {% for summary in related_objects_count.items %} +
  • {{summary.0|capfirst}} : {{summary.1|capfirst}}
  • + {% endfor %} +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+ +
+ {% for key in model_map.keys %} +
+
+
+
+
    + {% with models_dict=model_map|get_item:key %} + {% for item in models_dict.items %} +
  • + {{item.0.verbose_name}} + +
    + +
  • + {% endfor %} + {% endwith %} +
+
+
+
+
+
+

{% trans "Action Required" %}⚠️

+
+
+
+
+
+
+
+
+
+
+
+ {% endfor %} +
+
+
+
+ + + \ No newline at end of file diff --git a/horilla_views/templatetags/generic_template_filters.py b/horilla_views/templatetags/generic_template_filters.py index 3635755be..dc039b109 100644 --- a/horilla_views/templatetags/generic_template_filters.py +++ b/horilla_views/templatetags/generic_template_filters.py @@ -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(" ", "") diff --git a/horilla_views/urls.py b/horilla_views/urls.py index 3094592ff..074ba816f 100644 --- a/horilla_views/urls.py +++ b/horilla_views/urls.py @@ -39,4 +39,9 @@ urlpatterns = [ views.LastAppliedFilter.as_view(), name="last-applied-filter", ), + path( + "generic-delete", + views.HorillaDeleteConfirmationView.as_view(), + name="generic-delete", + ), ] diff --git a/horilla_views/views.py b/horilla_views/views.py index 9c0257b5e..5b0e939aa 100644 --- a/horilla_views/views.py +++ b/horilla_views/views.py @@ -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)} + (In {field.verbose_name}) + """ + 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""" + (Record in {",".join(verbose_names)}) + """ + ) + 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( + """ + + """ + ) + + def get_context_data(self, **kwargs) -> dict: + context = {} + context["confirmation_target"] = self.confirmation_target + return context diff --git a/leave/views.py b/leave/views.py index be82b3708..246f80bc7 100644 --- a/leave/views.py +++ b/leave/views.py @@ -27,6 +27,7 @@ from base.forms import PenaltyAccountForm from base.methods import ( choosesubordinates, closest_numbers, + eval_validate, export_data, filtersubordinates, get_key_instances, @@ -4736,7 +4737,7 @@ if apps.is_installed("attendance"): comp_leave_req = CompensatoryLeaveRequest.objects.get(id=comp_leave_id) context = { "comp_leave_req": comp_leave_req, - "my_request": eval(request.GET.get("my_request")), + "my_request": eval_validate(request.GET.get("my_request")), "instances_ids": requests_ids_json, "previous": previous_id, "next": next_id, diff --git a/offboarding/views.py b/offboarding/views.py index ef533ea55..5a7a01772 100644 --- a/offboarding/views.py +++ b/offboarding/views.py @@ -11,8 +11,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from base.context_processors import intial_notice_period -from base.methods import closest_numbers, sortby -from base.views import paginator_qry +from base.methods import closest_numbers, eval_validate, paginator_qry, sortby from employee.models import Employee from horilla.decorators import ( hx_request_required, @@ -178,7 +177,7 @@ def create_offboarding(request): """ Create offboarding view """ - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = None if instance_id and isinstance(instance_id, int): instance = Offboarding.objects.filter(id=instance_id).first() @@ -236,7 +235,7 @@ def create_stage(request): This method is used to create stages for offboardings """ offboarding_id = request.GET["offboarding_id"] - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = None if instance_id and isinstance(instance_id, int): instance = OffboardingStage.objects.get(id=instance_id) @@ -280,7 +279,7 @@ def add_employee(request): ) end_date = datetime.today() + timedelta(days=default_notice_period) stage_id = request.GET["stage_id"] - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = None if instance_id and isinstance(instance_id, int): instance = OffboardingEmployee.objects.get(id=instance_id) @@ -508,7 +507,7 @@ def add_task(request): This method is used to add offboarding tasks """ stage_id = request.GET.get("stage_id") - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) employees = OffboardingEmployee.objects.filter(stage_id=stage_id) instance = None if instance_id: @@ -807,7 +806,7 @@ def create_resignation_request(request): """ This method is used to render form to create resignation requests """ - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = None if instance_id: instance = ResignationLetter.objects.get(id=instance_id) diff --git a/payroll/views/component_views.py b/payroll/views/component_views.py index c47a5c2a3..25201a38f 100644 --- a/payroll/views/component_views.py +++ b/payroll/views/component_views.py @@ -29,6 +29,7 @@ import payroll.models.models from base.backends import ConfiguredEmailBackend from base.methods import ( closest_numbers, + eval_validate, filter_own_records, get_key_instances, get_next_month_same_date, @@ -1353,7 +1354,7 @@ def create_loan(request): """ This method is used to create and update the loan instance """ - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = LoanAccount.objects.filter(id=instance_id).first() form = forms.LoanAccountForm(instance=instance) if request.method == "POST": @@ -1593,7 +1594,7 @@ def create_reimbursement(request): """ This method is used to create reimbursement """ - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = None if instance_id: instance = Reimbursement.objects.filter(id=instance_id).first() @@ -1694,7 +1695,9 @@ def approve_reimbursements(request): status = request.GET["status"] if status == "canceled": status = "rejected" - amount = eval(request.GET.get("amount")) if request.GET.get("amount") else 0 + amount = ( + eval_validate(request.GET.get("amount")) if request.GET.get("amount") else 0 + ) amount = max(0, amount) reimbursements = Reimbursement.objects.filter(id__in=ids) if status and len(status): diff --git a/payroll/views/views.py b/payroll/views/views.py index dfd35cb56..ed873ae04 100644 --- a/payroll/views/views.py +++ b/payroll/views/views.py @@ -21,6 +21,7 @@ from django.utils.translation import gettext_lazy as _ from base.methods import ( closest_numbers, + eval_validate, export_data, generate_colors, generate_pdf, @@ -188,7 +189,7 @@ def contract_status_update(request, contract_id): @permission_required("payroll.change_contract") def bulk_contract_status_update(request): status = request.POST.get("status") - ids = eval(request.POST.get("ids")) + ids = eval_validate(request.POST.get("ids")) all_contracts = Contract.objects.all() contracts = all_contracts.filter(id__in=ids) @@ -1675,7 +1676,7 @@ def initial_notice_period(request): """ This method is used to set initial value notice period """ - notice_period = eval(request.GET["notice_period"]) + notice_period = eval_validate(request.GET["notice_period"]) settings = PayrollGeneralSetting.objects.first() settings = settings if settings else PayrollGeneralSetting() settings.notice_period = max(notice_period, 0) diff --git a/pms/views.py b/pms/views.py index 638d94bf8..58c351d65 100644 --- a/pms/views.py +++ b/pms/views.py @@ -27,6 +27,7 @@ from django.utils.translation import gettext_lazy as _ from base.methods import ( closest_numbers, + eval_validate, get_key_instances, get_pagination, paginator_qry, @@ -3221,8 +3222,8 @@ def key_result_current_value_update(request): This method is used to update keyresult current value """ try: - current_value = eval(request.POST.get("current_value")) - emp_kr_id = eval(request.POST.get("emp_key_result_id")) + current_value = eval_validate(request.POST.get("current_value")) + emp_kr_id = eval_validate(request.POST.get("emp_key_result_id")) emp_kr = EmployeeKeyResult.objects.get(id=emp_kr_id) if current_value <= emp_kr.target_value: emp_kr.current_value = current_value @@ -3309,7 +3310,7 @@ def create_meetings(request): Post: it will redirect to view_meetings.html . """ - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = None initial = {"manager": request.user.employee_get, "employee_id": None} if instance_id and isinstance(instance_id, int): diff --git a/recruitment/views/views.py b/recruitment/views/views.py index fb597519a..39aca26d2 100644 --- a/recruitment/views/views.py +++ b/recruitment/views/views.py @@ -43,7 +43,13 @@ from base.backends import ConfiguredEmailBackend from base.context_processors import check_candidate_self_tracking from base.countries import country_arr, s_a, states from base.forms import MailTemplateForm -from base.methods import export_data, generate_pdf, get_key_instances, sortby +from base.methods import ( + eval_validate, + export_data, + generate_pdf, + get_key_instances, + sortby, +) from base.models import EmailLog, HorillaMailTemplate, JobPosition from employee.models import Employee, EmployeeWorkInformation from horilla import settings @@ -1718,7 +1724,7 @@ def form_send_mail(request, cand_id=None): candidate_obj = None stage_id = None if request.GET.get("stage_id"): - stage_id = eval(request.GET.get("stage_id")) + stage_id = eval_validate(request.GET.get("stage_id")) if cand_id: candidate_obj = Candidate.objects.get(id=cand_id) candidates = Candidate.objects.all() @@ -2627,7 +2633,7 @@ def create_reject_reason(request): """ This method is used to create/update the reject reasons """ - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) instance = None if instance_id: instance = RejectReason.objects.get(id=instance_id) @@ -2856,7 +2862,7 @@ def create_skills(request): """ This method is used to create the skills """ - instance_id = eval(str(request.GET.get("instance_id"))) + instance_id = eval_validate(str(request.GET.get("instance_id"))) dynamic = request.GET.get("dynamic") hx_vals = request.GET.get("data") instance = None @@ -2920,7 +2926,7 @@ def view_bulk_resumes(request): """ This function returns the bulk_resume.html page to the modal """ - rec_id = eval(str(request.GET.get("rec_id"))) + rec_id = eval_validate(str(request.GET.get("rec_id"))) resumes = Resume.objects.filter(recruitment_id=rec_id) return render( @@ -2935,7 +2941,7 @@ def add_bulk_resumes(request): """ This function is used to create bulk resume """ - rec_id = eval(str(request.GET.get("rec_id"))) + rec_id = eval_validate(str(request.GET.get("rec_id"))) recruitment = Recruitment.objects.get(id=rec_id) if request.method == "POST": files = request.FILES.getlist("files")