[IMP] HORILLA AUDIT: Added new app for tracking changes to values
This commit is contained in:
1
horilla_audit/__init__.py
Normal file
1
horilla_audit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from horilla_audit import settings
|
||||
9
horilla_audit/admin.py
Normal file
9
horilla_audit/admin.py
Normal 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
6
horilla_audit/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HorillaAuditConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'horilla_audit'
|
||||
31
horilla_audit/context_processors.py
Normal file
31
horilla_audit/context_processors.py
Normal 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
48
horilla_audit/forms.py
Normal 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
113
horilla_audit/methods.py
Normal 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
|
||||
0
horilla_audit/migrations/__init__.py
Normal file
0
horilla_audit/migrations/__init__.py
Normal file
129
horilla_audit/models.py
Normal file
129
horilla_audit/models.py
Normal 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
11
horilla_audit/settings.py
Normal 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",
|
||||
)
|
||||
80
horilla_audit/templates/horilla_audit/horilla_audit_log.html
Normal file
80
horilla_audit/templates/horilla_audit/horilla_audit_log.html
Normal 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>
|
||||
0
horilla_audit/templatetags/__init__.py
Normal file
0
horilla_audit/templatetags/__init__.py
Normal file
19
horilla_audit/templatetags/audit_filters.py
Normal file
19
horilla_audit/templatetags/audit_filters.py
Normal 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
3
horilla_audit/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
horilla_audit/views.py
Normal file
3
horilla_audit/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user