From eb287897ab21382281f8fbdbff2138bd9f0c411b Mon Sep 17 00:00:00 2001 From: Horilla Date: Thu, 26 Oct 2023 12:51:28 +0530 Subject: [PATCH] [IMP] HORILLA AUDIT: Added new app for tracking changes to values --- horilla_audit/__init__.py | 1 + horilla_audit/admin.py | 9 ++ horilla_audit/apps.py | 6 + horilla_audit/context_processors.py | 31 +++++ horilla_audit/forms.py | 48 +++++++ horilla_audit/methods.py | 113 +++++++++++++++ horilla_audit/migrations/__init__.py | 0 horilla_audit/models.py | 129 ++++++++++++++++++ horilla_audit/settings.py | 11 ++ .../horilla_audit/horilla_audit_log.html | 80 +++++++++++ horilla_audit/templatetags/__init__.py | 0 horilla_audit/templatetags/audit_filters.py | 19 +++ horilla_audit/tests.py | 3 + horilla_audit/views.py | 3 + 14 files changed, 453 insertions(+) create mode 100644 horilla_audit/__init__.py create mode 100644 horilla_audit/admin.py create mode 100644 horilla_audit/apps.py create mode 100644 horilla_audit/context_processors.py create mode 100644 horilla_audit/forms.py create mode 100644 horilla_audit/methods.py create mode 100644 horilla_audit/migrations/__init__.py create mode 100644 horilla_audit/models.py create mode 100644 horilla_audit/settings.py create mode 100644 horilla_audit/templates/horilla_audit/horilla_audit_log.html create mode 100644 horilla_audit/templatetags/__init__.py create mode 100644 horilla_audit/templatetags/audit_filters.py create mode 100644 horilla_audit/tests.py create mode 100644 horilla_audit/views.py diff --git a/horilla_audit/__init__.py b/horilla_audit/__init__.py new file mode 100644 index 000000000..b2223c926 --- /dev/null +++ b/horilla_audit/__init__.py @@ -0,0 +1 @@ +from horilla_audit import settings \ No newline at end of file diff --git a/horilla_audit/admin.py b/horilla_audit/admin.py new file mode 100644 index 000000000..aa789e487 --- /dev/null +++ b/horilla_audit/admin.py @@ -0,0 +1,9 @@ +""" +admin.py +""" +from django.contrib import admin +from horilla_audit.models import HorillaAuditLog, HorillaAuditInfo, AuditTag + +# Register your models here. + +admin.site.register(AuditTag) diff --git a/horilla_audit/apps.py b/horilla_audit/apps.py new file mode 100644 index 000000000..ce340c2d9 --- /dev/null +++ b/horilla_audit/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HorillaAuditConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'horilla_audit' diff --git a/horilla_audit/context_processors.py b/horilla_audit/context_processors.py new file mode 100644 index 000000000..00da1c1e5 --- /dev/null +++ b/horilla_audit/context_processors.py @@ -0,0 +1,31 @@ +""" +context_processor.py + +This module is used to register context processor` +""" +from django.urls import path, include +from django.http import JsonResponse +from horilla_audit.forms import HistoryForm +from horilla_audit.models import AuditTag +from horilla.urls import urlpatterns + + +def history_form(request): + """ + This method will return the history additional field form + """ + form = HistoryForm() + return {"history_form": form} + + +def dynamic_tag(request): + """ + This method is used to dynamically create history tags + """ + + title = request.POST["title"] + title = AuditTag.objects.get_or_create(title=title) + return JsonResponse({"id": title[0].id}) + + +urlpatterns.append(path("horilla-audit-log", dynamic_tag, name="horilla-audit-log")) diff --git a/horilla_audit/forms.py b/horilla_audit/forms.py new file mode 100644 index 000000000..a82d5fceb --- /dev/null +++ b/horilla_audit/forms.py @@ -0,0 +1,48 @@ +""" +forms.py +""" +from collections.abc import Mapping +from typing import Any +from django import forms +from django.forms.utils import ErrorList +from django.template.loader import render_to_string +from horilla_audit.models import HorillaAuditInfo, AuditTag + + +class HistoryForm(forms.Form): + """ + HistoryForm + """ + + history_title = forms.CharField(required=False) + history_description = forms.CharField( + widget=forms.Textarea( + attrs={"placeholder": "Enter text", "class": "oh-input w-100", "rows": "2"} + ), + required=False, + ) + history_highlight = forms.BooleanField(required=False) + history_tags = forms.ModelMultipleChoiceField( + queryset=AuditTag.objects.all(), required=False + ) + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.initial = {} + self.fields["history_title"].widget.attrs.update({"class": "oh-input w-100"}) + self.fields["history_highlight"].widget.attrs.update({"style": "display:block"}) + self.fields["history_tags"].widget.attrs.update( + { + "class": "oh-select oh--dynamic-select-2", + "style": "width:100%", + "data-ajax-name": "auditDynamicTag", + } + ) + + def as_history_modal(self, *args, **kwargs): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + context = {"form": self} + table_html = render_to_string("horilla_audit/horilla_audit_log.html", context) + return table_html diff --git a/horilla_audit/methods.py b/horilla_audit/methods.py new file mode 100644 index 000000000..c0c40c958 --- /dev/null +++ b/horilla_audit/methods.py @@ -0,0 +1,113 @@ +""" +methods.py + +This module is used to write methods related to the history +""" +from django.db import models +from django.contrib.auth.models import User + + +class Bot: + def __init__(self) -> None: + self.__str__() + + def __str__(self) -> str: + return "Horilla Bot" + + def get_avatar(self): + return "https://ui-avatars.com/api/?name=Horilla+Bot&background=random" + + +def _check_and_delete(entry1, entry2, dry_run=False): + delta = entry1.diff_against(entry2) + if not delta.changed_fields: + if not dry_run: + entry1.delete() + return 1 + return 0 + + +def remove_duplicate_history(instance): + """ + This method is used to remove duplicate entries + """ + o_qs = instance.history_set.all() + entries_deleted = 0 + # ordering is ('-history_date', '-history_id') so this is ok + f1 = o_qs.first() + if not f1: + return + for f2 in o_qs[1:]: + entries_deleted += _check_and_delete( + f1, + f2, + ) + f1 = f2 + + +def get_field_label(model_class, field_name): + # Check if the field exists in the model class + if hasattr(model_class, field_name): + field = model_class._meta.get_field(field_name) + return field.verbose_name.capitalize() + # Return None if the field does not exist + return None + + +def get_diff(instance): + """ + This method is used to find the differences in the history + """ + remove_duplicate_history(instance) + history = instance.history_set.all() + history_list = list(history) + pairs = [ + [history_list[i], history_list[i + 1]] for i in range(len(history_list) - 1) + ] + delta_changes = [] + create_history = history.filter(history_type="+").first() + for pair in pairs: + delta = pair[0].diff_against(pair[1]) + diffs = [] + class_name = pair[0].instance.__class__ + for change in delta.changes: + old = change.old + new = change.new + field = instance._meta.get_field(change.field) + is_fk = False + if isinstance(field, models.ForeignKey): + is_fk = True + # old = getattr(pair[0], change.field) + # new = getattr(pair[1], change.field) + diffs.append( + { + "field": get_field_label(class_name, change.field), + "field_name": change.field, + "is_fk": is_fk, + "old": old, + "new": new, + } + ) + updated_by = ( + User.objects.get(id=pair[0].history_user.id).employee_get + if pair[0].history_user + else Bot() + ) + delta_changes.append( + { + "type": "Changes", + "pair": pair, + "changes": diffs, + "updated_by": updated_by, + } + ) + if create_history: + updated_by = create_history.history_user.employee_get + delta_changes.append( + { + "type": f"{create_history.instance.__class__._meta.verbose_name.capitalize()} created", + "pair": (create_history, create_history), + "updated_by": updated_by, + } + ) + return delta_changes diff --git a/horilla_audit/migrations/__init__.py b/horilla_audit/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/horilla_audit/models.py b/horilla_audit/models.py new file mode 100644 index 000000000..e3a6e6b88 --- /dev/null +++ b/horilla_audit/models.py @@ -0,0 +1,129 @@ +""" +models.py +""" +from collections.abc import Iterable +from django.db import models +from django.dispatch import receiver +from simple_history.models import ( + HistoricalRecords, + _default_get_user, + _history_user_getter, + _history_user_setter, +) +from simple_history.signals import ( + post_create_historical_record, + pre_create_historical_record, + # pre_create_historical_m2m_records, + # post_create_historical_m2m_records, +) + +# from employee.models import Employee +from horilla_audit.methods import remove_duplicate_history + + +# Create your models here. + + +class AuditTag(models.Model): + """ + HistoryTag model + """ + + title = models.CharField(max_length=20) + highlight = models.BooleanField(default=False) + + def __str__(self) -> str: + return str(self.title) + + class Meta: + """ + Meta class for aditional info + """ + + app_label = "horilla_audit" + + +class HorillaAuditInfo(models.Model): + """ + HorillaAuditInfo model to store additional info + """ + + history_title = models.CharField(max_length=20, null=True, blank=True) + history_description = models.TextField(null=True) + history_highlight = models.BooleanField(default=False, null=True) + history_tags = models.ManyToManyField(AuditTag) + + class Meta: + """ + Meta class for aditional info + """ + + app_label = "horilla_audit" + abstract = True + + +class HorillaAuditLog(HistoricalRecords): + """ + Model to store additional information for historical records. + """ + + # def __init__(self, *args, bases=None, **kwargs): + # super(HorillaAuditLog, self).__init__(*args, **kwargs) + # self.is_horilla_audit_log = True + + pass + + # history_comments = models.ManyToManyField("HistoryComment", blank=True) + + +@receiver(pre_create_historical_record) +def pre_create_horilla_audit_log(sender, instance, *args, **kwargs): + """ + Pre create horill audit log method + """ + try: + history_instance = kwargs["history_instance"] + history_instance.history_title = HistoricalRecords.thread.request.POST.get( + "history_title" + ) + history_instance.history_description = ( + HistoricalRecords.thread.request.POST.get("history_description") + ) + history_instance.history_highlight = ( + True + if HistoricalRecords.thread.request.POST.get("history_highlight") == "on" + else False + ) + instance.skip_history = True + except: + pass + + +@receiver(post_create_historical_record) +def post_create_horilla_audit_log(sender, instance, *_args, **kwargs): + """ + Post create horill audit log method + """ + try: + history_instance = kwargs["history_instance"] + history_instance.history_tags.set( + HistoricalRecords.thread.request.POST.getlist("history_tags") + ) + if isinstance(history_instance, HorillaAuditLog): + history_instance.history_title = "Demo Title" + remove_duplicate_history(instance) + if instance.skip_history: + instance.history_set.filter(pk=history_instance.pk).delete() + kwargs["history_instance"] = None + except: + pass + + +# class HistoryComment(models.Model): +# """ +# HistoryComment model +# """ + +# employee_id = models.ForeignKey("Employee", on_delete=models.PROTECT) +# history_id = models.ForeignKey(HorillaAuditLog, on_delete=models.PROTECT) +# message = models.TextField() diff --git a/horilla_audit/settings.py b/horilla_audit/settings.py new file mode 100644 index 000000000..a0e3854ec --- /dev/null +++ b/horilla_audit/settings.py @@ -0,0 +1,11 @@ +""" +horilla_audit/settings.py + +This module is used to write settings contents related to payroll app +""" + +from horilla.settings import TEMPLATES + +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "horilla_audit.context_processors.history_form", +) diff --git a/horilla_audit/templates/horilla_audit/horilla_audit_log.html b/horilla_audit/templates/horilla_audit/horilla_audit_log.html new file mode 100644 index 000000000..fc18d1648 --- /dev/null +++ b/horilla_audit/templates/horilla_audit/horilla_audit_log.html @@ -0,0 +1,80 @@ +{% load i18n %}{% load widget_tweaks %} {% load attendancefilters %} + + + diff --git a/horilla_audit/templatetags/__init__.py b/horilla_audit/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/horilla_audit/templatetags/audit_filters.py b/horilla_audit/templatetags/audit_filters.py new file mode 100644 index 000000000..c4cb9f6c8 --- /dev/null +++ b/horilla_audit/templatetags/audit_filters.py @@ -0,0 +1,19 @@ +from django.template.defaultfilters import register +from django import template +from employee.models import Employee, EmployeeWorkInformation +from django.core.paginator import Page, Paginator + + +@register.filter(name="fk_history") +def fk_history(instance, change): + """ + This method is used to return str of the fk fields + """ + value = "Deleted" + try: + value = getattr(instance, change["field_name"]) + except: + value = instance.__dict__[change["field_name"] + "_id"] + value = str(value) + f" (Previous {change['field']} deleted)" + pass + return value diff --git a/horilla_audit/tests.py b/horilla_audit/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/horilla_audit/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/horilla_audit/views.py b/horilla_audit/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/horilla_audit/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.