[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 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",
|
||||
),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user