[ADD] HORILLA VIEWS: Horilla History View

This commit is contained in:
Horilla
2025-04-21 18:07:18 +05:30
parent c9d80372c4
commit 7809a7803a
4 changed files with 339 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
"""
horilla_views/generic/cbv/history.py
"""
from django.views.generic import DetailView
from django.contrib import messages
from django.apps import apps
from django.utils.decorators import method_decorator
from simple_history.utils import get_history_model_for_model
from horilla_views.history_methods import get_diff
from horilla_views.generic.cbv.views import HorillaFormView
from horilla_views.cbv_methods import hx_request_required
from horilla.horilla_middlewares import _thread_locals
@method_decorator(hx_request_required, name="dispatch")
class HorillaHistoryView(DetailView):
"""
GenericHorillaProfileView
"""
template_name = "generic/horilla_history_view.html"
has_perm_to_revert = False
fields: list = []
history_related_name = "history"
def get_context_data(self, **kwargs):
"""
Get context data
"""
context = super().get_context_data(**kwargs)
instance = self.get_object()
context["tracking"] = get_diff(instance, self.history_related_name)
context["model"] = (
f"{self.model._meta.app_label}.{self.model._meta.object_name}"
)
context["has_perm_to_revert"] = self.has_perm_to_revert
return context
def __init__(self, **kwargs):
super().__init__(**kwargs)
request = getattr(_thread_locals, "request", None)
self.request = request
def post(self, request, history_id, *args, **kwargs):
"""
Revert
"""
app, model = request.GET["model"].split(".")
self.model = apps.get_model(app, model)
history = get_history_model_for_model(self.model).objects.get(
history_id=history_id
)
history.instance.save()
messages.success(request, "History reverted")
return HorillaFormView.HttpResponse()

View File

@@ -0,0 +1,179 @@
"""
methods.py
This module is used to write methods related to the history
"""
from django.contrib.auth.models import User
from django.core.paginator import Paginator
from django.db import models
from django.shortcuts import render
from horilla.decorators import apply_decorators
class Bot:
def __init__(self) -> None:
self.__str__()
def __str__(self) -> str:
return "Horilla Bot"
def get_avatar(self):
"""
Get avatar
"""
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, history_related_name):
"""
This method is used to remove duplicate entries
"""
o_qs = getattr(instance, history_related_name).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 filter_history(histories, track_fields):
filtered_histories = []
for history in histories:
changes = history.get("changes", [])
filtered_changes = [
change for change in changes if change.get("field_name", "") in track_fields
]
if filtered_changes:
history["changes"] = filtered_changes
filtered_histories.append(history)
histories = filtered_histories
return histories
def get_diff(instance, history_related_name):
"""
This method is used to find the differences in the history
"""
remove_duplicate_history(instance, history_related_name)
history = getattr(instance, history_related_name).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.fields.CharField)
and field.choices
and old
and new
):
choices = dict(field.choices)
old = choices[old]
new = choices[new]
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:
try:
updated_by = create_history.history_user.employee_get
except:
updated_by = Bot()
delta_changes.append(
{
"type": f"{create_history.instance.__class__._meta.verbose_name.capitalize()} created",
"pair": (create_history, create_history),
"updated_by": updated_by,
}
)
if instance._meta.model_name == "employeeworkinformation":
from .models import HistoryTrackingFields
history_tracking_instance = HistoryTrackingFields.objects.first()
if history_tracking_instance and history_tracking_instance.tracking_fields:
track_fields = history_tracking_instance.tracking_fields["tracking_fields"]
if track_fields:
delta_changes = filter_history(delta_changes, track_fields)
return delta_changes
def history_tracking(request, obj_id, **kwargs):
model = kwargs.get("model")
decorator_strings = kwargs.get("decorators", [])
@apply_decorators(decorator_strings)
def _history_tracking(request, obj_id, model):
instance = model.objects.get(pk=obj_id)
histories = instance.horilla_history.all()
page_number = request.GET.get("page", 1)
paginator = Paginator(histories, 4)
page_obj = paginator.get_page(page_number)
context = {
"histories": page_obj,
"model_name": model,
}
return render(
request,
"horilla_audit/history_tracking.html",
context,
)
return _history_tracking(request, obj_id, model)

View File

@@ -0,0 +1,96 @@
<div id="generic-history-container">
<button hidden hx-get="{{ request.path }}" class="reload-record" hx-target="#generic-history-container" hx-swap="outerHTML"></button>
{% load static %} {% load i18n %}
{% load audit_filters %}
<div class="oh-activity-sidebar__header mt-5" style="position: sticky; top: 0; z-index: 100; background-color: #fff;padding-bottom: 10px;">
<a style="cursor: pointer;" onclick="$('.oh-activity-sidebar--show').removeClass('oh-activity-sidebar--show');"><ion-icon name="chevron-forward-outline" class="oh-activity-sidebar__header-icon me-2 oh-activity-sidebar__close md flip-rtl hydrated" data-target="#activitySidebar" role="img"></ion-icon></a>
<span class="oh-activity-sidebar__title fw-bold">{{ object }}'s Logs</span>
</div>
<div class="row">
{% if tracking %}
{% for history in tracking %}
<div class="oh-history__container">
<div class="oh-history_date oh-card__title oh-card__title--sm fw-bold me-2">
<span class="oh-history_date-content">
<span class="dateformat_changer">{{ history.pair.1.history_date|date:'M. d, Y' }}</span>&nbsp,&nbsp
<span class="timeformat_changer">{{ history.pair.1.history_date|date:'g:i A' }}</span>
</span>
</div>
<div class="d-flex">
<div class="oh-history_user-img">
<img src="{{ history.updated_by.get_avatar }}" alt="" class="oh-history_user-pic" />
</div>
<div class="oh-history_user-details">
<span class="oh-history__username">{{ history.updated_by }}</span>
<div class="oh-history_abt pb-0">
{% if history.pair.0.history_title %}
<span class="oh-history_task-state">{{ history.pair.0.history_title }}</span>
{% endif %}
{% if history.pair.0.history_description %}
<p class="oh-history_time">{{ history.pair.0.history_description }}</p>
{% endif %}
<div class="oh-history_tabs">
{% for tag in history.pair.0.history_tags.all %}
<a href="#" class="oh-history_msging-email oh-history_tabs-items">{{ tag.title }}</a>
{% endfor %}
</div>
</div>
<div class="oh-history_msg-container">
<div class="oh-history_task-tracker">
<span class="oh-history_msging-email">
{{ history.type }}
{% if has_perm_to_revert %}
<button hidden id="revert{{history.pair.1.pk}}" hx-post="{% url 'history-revert' object.pk history.pair.1.pk %}?model={{ model }}"></button>
{% if history.pair.1.pk %}
<a href="javascript:void(0);" title="Restore the previous state"
onclick="Swal.fire({
title: 'Are you sure?',
text: 'This will revert the object to a previous state.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, revert it'
}).then(result => {
if (result.isConfirmed) {
document.getElementById('revert{{ history.pair.1.pk }}').click();
}
})">
<ion-icon name="git-pull-request-outline"></ion-icon>
</a>
{% endif %}
{% endif %}
</span>
<ul class="ul">
{% for change in history.changes %}
<li class="oh-history_task-list">
<div class="oh-history_track-value">
<span>{{ history.pair.1|fk_history:change }}</span>
<img src="{% static '/images/ui/arrow-right-line.svg' %}" class="oh-progress_arrow" alt="" />
<span class="oh-history_tracking-value">{{ history.pair.0|fk_history:change }}</span>
<span class="oh-history-task-state"><i>({{ change.field }})</i></span>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="oh-wrapper" align="center" style="margin-top: 7vh; margin-bottom:7vh;">
<div align="center">
<img src="{% static 'images/ui/history.png' %}" class="oh-404__image" alt="Page not found. 404." />
<h5 class="oh-404__subtitle mt-4 ml-2">{% trans 'No history found.' %}</h5>
</div>
</div>
{% comment %} <div class="d-flex justify-content-center align-items-center" style="height: 40vh">
<h5 class="oh-404__subtitle">{% trans 'No history found.' %}</h5>
</div> {% endcomment %}
{% endif %}
</div>
</div>

View File

@@ -5,6 +5,7 @@ horilla_views/urls.py
from django.urls import path
from horilla_views import views
from horilla_views.generic.cbv import history
from horilla_views.generic.cbv.views import ReloadMessages
urlpatterns = [
@@ -44,4 +45,9 @@ urlpatterns = [
views.HorillaDeleteConfirmationView.as_view(),
name="generic-delete",
),
path(
"horilla-history-revert/<int:pk>/<int:history_id>/",
history.HorillaHistoryView.as_view(),
name="history-revert",
),
]