diff --git a/horilla_views/generic/cbv/history.py b/horilla_views/generic/cbv/history.py new file mode 100644 index 000000000..0769ec69a --- /dev/null +++ b/horilla_views/generic/cbv/history.py @@ -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() diff --git a/horilla_views/history_methods.py b/horilla_views/history_methods.py new file mode 100644 index 000000000..6496d0ee3 --- /dev/null +++ b/horilla_views/history_methods.py @@ -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) diff --git a/horilla_views/templates/generic/horilla_history_view.html b/horilla_views/templates/generic/horilla_history_view.html new file mode 100644 index 000000000..8447e790a --- /dev/null +++ b/horilla_views/templates/generic/horilla_history_view.html @@ -0,0 +1,96 @@ +
{{ history.pair.0.history_description }}
+ {% endif %} +
+