[ADD] HORILLA VIEWS: Horilla History View
This commit is contained in:
58
horilla_views/generic/cbv/history.py
Normal file
58
horilla_views/generic/cbv/history.py
Normal 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()
|
||||||
179
horilla_views/history_methods.py
Normal file
179
horilla_views/history_methods.py
Normal 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)
|
||||||
96
horilla_views/templates/generic/horilla_history_view.html
Normal file
96
horilla_views/templates/generic/horilla_history_view.html
Normal 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> , 
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ horilla_views/urls.py
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from horilla_views import views
|
from horilla_views import views
|
||||||
|
from horilla_views.generic.cbv import history
|
||||||
from horilla_views.generic.cbv.views import ReloadMessages
|
from horilla_views.generic.cbv.views import ReloadMessages
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -44,4 +45,9 @@ urlpatterns = [
|
|||||||
views.HorillaDeleteConfirmationView.as_view(),
|
views.HorillaDeleteConfirmationView.as_view(),
|
||||||
name="generic-delete",
|
name="generic-delete",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"horilla-history-revert/<int:pk>/<int:history_id>/",
|
||||||
|
history.HorillaHistoryView.as_view(),
|
||||||
|
name="history-revert",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user