[IMP] HORILLA AUDIT: Added new app for tracking changes to values

This commit is contained in:
Horilla
2023-10-26 12:51:28 +05:30
parent dfcbbc7b54
commit eb287897ab
14 changed files with 453 additions and 0 deletions

View File

@@ -0,0 +1 @@
from horilla_audit import settings

9
horilla_audit/admin.py Normal file
View File

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

6
horilla_audit/apps.py Normal file
View File

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

View File

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

48
horilla_audit/forms.py Normal file
View File

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

113
horilla_audit/methods.py Normal file
View File

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

View File

129
horilla_audit/models.py Normal file
View File

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

11
horilla_audit/settings.py Normal file
View File

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

View File

@@ -0,0 +1,80 @@
{% load i18n %}{% load widget_tweaks %} {% load attendancefilters %}
<script>
function auditDynamicTag(optionElement) {
var val = optionElement.val();
$.ajax({
type: "post",
url: "/horilla-audit-log",
data: {
csrfmiddlewaretoken: getCookie("csrftoken"),
title: val,
},
success: function (response) {
optionElement.val(response.id);
},
});
}
</script>
<div
class="oh-modal"
id="historyModal"
role="dialog"
aria-labelledby="historyModal"
aria-hidden="true"
>
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<h2 class="oh-modal__dialog-title" id="historyModalLabel">
{% trans "Why this change?" %}
</h2>
<a
class="oh-modal__close"
aria-label="Close"
onclick='$("#historyModal").removeClass("oh-modal--show")'
>
<ion-icon name="close-outline"></ion-icon>
</a>
</div>
<div class="oh-modal__dialog-body" id="historyModalBody">
{{form.as_p}}
<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 history-modal-button"
>
{% trans "Save" %}
</button>
</div>
</div>
</div>
</div>
<script>
$("#historyFormContainer").ready(function () {
$("#historyFormContainer")
.find("select[multiple]")
.select2({
createTag: function (params) {
var term = $.trim(params.term);
if (term === "") {
return null;
}
jq;
return {
id: term,
text: term,
newTag: true, // add additional parameters
};
},
tags: true,
tokenSeparators: [",", " "],
});
$("form[method=post] button").click(function (e) {
$("#historyModal").addClass("oh-modal--show");
if (!$(this).hasClass("history-modal-button")) {
e.preventDefault();
}
});
});
</script>

View File

View File

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

3
horilla_audit/tests.py Normal file
View File

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

3
horilla_audit/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.