8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-27 07:02:26 +00:00

Start implementing detailed history for event class

This commit is contained in:
Jean-Romain Garnier 2020-04-25 12:31:18 +02:00 committed by Gabriel Detraz
parent 147ff29fb5
commit c43c532822
8 changed files with 83 additions and 98 deletions

View file

@ -92,6 +92,10 @@ class ActionsSearch:
return classes return classes
############################
# Machine history search #
############################
class MachineHistorySearchEvent: class MachineHistorySearchEvent:
def __init__(self, user, machine, interface, start=None, end=None): def __init__(self, user, machine, interface, start=None, end=None):
""" """
@ -280,14 +284,19 @@ class MachineHistorySearch:
return self.events return self.events
############################
# Generic history classes #
############################
class RelatedHistory: class RelatedHistory:
def __init__(self, name, model_name, object_id): def __init__(self, name, app_name, model_name, object_id):
""" """
:param name: Name of this instance :param name: Name of this instance
:param model_name: Name of the related model (e.g. "user") :param model_name: Name of the related model (e.g. "user")
:param object_id: ID of the related object :param object_id: ID of the related object
""" """
self.name = "{} (id = {})".format(name, object_id) self.name = "{} (id = {})".format(name, object_id)
self.app_name = app_name
self.model_name = model_name self.model_name = model_name
self.object_id = object_id self.object_id = object_id
@ -417,6 +426,10 @@ class History:
self._last_version = version self._last_version = version
############################
# Revision history #
############################
class VersionAction(HistoryEvent): class VersionAction(HistoryEvent):
def __init__(self, version): def __init__(self, version):
self.version = version self.version = version
@ -496,6 +509,10 @@ class RevisionAction:
return self.revision.get_comment() return self.revision.get_comment()
############################
# Class-specific history #
############################
class UserHistoryEvent(HistoryEvent): class UserHistoryEvent(HistoryEvent):
def _repr(self, name, value): def _repr(self, name, value):
""" """
@ -588,7 +605,7 @@ class UserHistory(History):
super(UserHistory, self).__init__() super(UserHistory, self).__init__()
self.event_type = UserHistoryEvent self.event_type = UserHistoryEvent
def get(self, user_id): def get(self, user_id, model):
""" """
:param user_id: int, the id of the user to lookup :param user_id: int, the id of the user to lookup
:return: list or None, a list of UserHistoryEvent, in reverse chronological order :return: list or None, a list of UserHistoryEvent, in reverse chronological order
@ -633,7 +650,8 @@ class UserHistory(History):
) )
self.related = [RelatedHistory( self.related = [RelatedHistory(
m.field_dict["name"] or _("None"), m.field_dict["name"] or _("None"),
"machine", "machines"
"Machine",
m.field_dict["id"]) for m in self.related] m.field_dict["id"]) for m in self.related]
self.related = list(dict.fromkeys(self.related)) self.related = list(dict.fromkeys(self.related))
@ -716,7 +734,7 @@ class MachineHistory(History):
super(MachineHistory, self).__init__() super(MachineHistory, self).__init__()
self.event_type = MachineHistoryEvent self.event_type = MachineHistoryEvent
def get(self, machine_id): def get(self, machine_id, model):
# Add as "related" histories the list of Interface objects # Add as "related" histories the list of Interface objects
# that were once assigned to this machine # that were once assigned to this machine
self.related = list( self.related = list(
@ -728,7 +746,8 @@ class MachineHistory(History):
# Create RelatedHistory objects and remove duplicates # Create RelatedHistory objects and remove duplicates
self.related = [RelatedHistory( self.related = [RelatedHistory(
i.field_dict["mac_address"] or _("None"), i.field_dict["mac_address"] or _("None"),
"interface", "machines",
"Interface",
i.field_dict["id"]) for i in self.related] i.field_dict["id"]) for i in self.related]
self.related = list(dict.fromkeys(self.related)) self.related = list(dict.fromkeys(self.related))
@ -782,10 +801,34 @@ class InterfaceHistory(History):
super(InterfaceHistory, self).__init__() super(InterfaceHistory, self).__init__()
self.event_type = InterfaceHistoryEvent self.event_type = InterfaceHistoryEvent
def get(self, interface_id): def get(self, interface_id, model):
events = super(InterfaceHistory, self).get(interface_id, Interface) events = super(InterfaceHistory, self).get(interface_id, Interface)
# Update name # Update name
self.name = self._last_version.field_dict["mac_address"] self.name = self._last_version.field_dict["mac_address"]
return events return events
############################
# History auto-detect #
############################
HISTORY_CLASS_MAPPING = {
User: UserHistory,
Machine: MachineHistory,
Interface: InterfaceHistory,
"default": History
}
def get_history_class(model):
"""
Find the mos appropriate History subclass to represent
the given model's history
:model: class
"""
try:
return HISTORY_CLASS_MAPPING[model]()
except KeyError:
return HISTORY_CLASS_MAPPING["default"]()

View file

@ -82,7 +82,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<ul> <ul>
{% for related in related_history %} {% for related in related_history %}
<li> <li>
<a title="{% trans "History" %}" href="{% url 'logs:detailed-history' related.model_name related.object_id %}">{{ related.model_name }} - {{ related.name }}</a> <a title="{% trans "History" %}" href="{% url 'logs:history' related.app_name related.model_name related.object_id %}">{{ related.model_name }} - {{ related.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -42,7 +42,7 @@ def is_facture(baseinvoice):
@register.inclusion_tag("buttons/history.html") @register.inclusion_tag("buttons/history.html")
def history_button(instance, text=False, detailed=False, html_class=True): def history_button(instance, text=False, html_class=True):
"""Creates the correct history button for an instance. """Creates the correct history button for an instance.
Args: Args:
@ -57,6 +57,5 @@ def history_button(instance, text=False, detailed=False, html_class=True):
"name": instance._meta.model_name, "name": instance._meta.model_name,
"id": instance.id, "id": instance.id,
"text": text, "text": text,
"detailed": detailed,
"class": html_class, "class": html_class,
} }

View file

@ -42,11 +42,6 @@ urlpatterns = [
views.history, views.history,
name="history", name="history",
), ),
url(
r"(?P<object_name>\w+)/(?P<object_id>[0-9]+)$",
views.detailed_history,
name="detailed-history",
),
url(r"^stats_general/$", views.stats_general, name="stats-general"), url(r"^stats_general/$", views.stats_general, name="stats-general"),
url(r"^stats_models/$", views.stats_models, name="stats-models"), url(r"^stats_models/$", views.stats_models, name="stats-models"),
url(r"^stats_users/$", views.stats_users, name="stats-users"), url(r"^stats_users/$", views.stats_users, name="stats-users"),

View file

@ -37,7 +37,6 @@ nombre d'objets par models, nombre d'actions par user, etc
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from itertools import chain
from django.urls import reverse from django.urls import reverse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
@ -105,9 +104,7 @@ from .models import (
ActionsSearch, ActionsSearch,
RevisionAction, RevisionAction,
MachineHistorySearch, MachineHistorySearch,
UserHistory, get_history_class
MachineHistory,
InterfaceHistory
) )
from .forms import ( from .forms import (
@ -526,33 +523,24 @@ def stats_search_machine_history(request):
return render(request, "logs/search_machine_history.html", {"history_form": history_form}) return render(request, "logs/search_machine_history.html", {"history_form": history_form})
def get_history_object(request, model, object_name, object_id, allow_deleted=False): def get_history_object(request, model, object_name, object_id):
"""Get the objet of type model with the given object_id """Get the objet of type model with the given object_id
Handles permissions and DoesNotExist errors Handles permissions and DoesNotExist errors
""" """
is_deleted = False
try: try:
object_name_id = object_name + "id" object_name_id = object_name + "id"
kwargs = {object_name_id: object_id} kwargs = {object_name_id: object_id}
instance = model.get_instance(**kwargs) instance = model.get_instance(**kwargs)
except model.DoesNotExist: except model.DoesNotExist:
is_deleted = True
instance = None instance = None
if is_deleted and not allow_deleted: if instance is None:
messages.error(request, _("Nonexistent entry.")) authorized = can_view_app("logs")
return False, redirect(
reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
if is_deleted:
can_view = can_view_app("logs")
msg = None msg = None
else: else:
can_view, msg, _permissions = instance.can_view(request.user) authorized, msg, _permissions = instance.can_view(request.user)
if not can_view: if not authorized:
messages.error( messages.error(
request, msg or _("You don't have the right to access this menu.") request, msg or _("You don't have the right to access this menu.")
) )
@ -563,61 +551,14 @@ def get_history_object(request, model, object_name, object_id, allow_deleted=Fal
return True, instance return True, instance
@login_required
def detailed_history(request, object_name, object_id):
"""Render a detailed history for a model.
Permissions are handled by get_history_object.
"""
# Only handle objects for which a detailed view exists
if object_name == "user":
model = User
history = UserHistory()
elif object_name == "machine":
model = Machine
history = MachineHistory()
elif object_name == "interface":
model = Interface
history = InterfaceHistory()
else:
raise Http404(_("No model found."))
# Get instance and check permissions
can_view, instance = get_history_object(request, model, object_name, object_id, allow_deleted=True)
if not can_view:
return instance
# Generate the pagination with the objects
max_result = GeneralOption.get_cached_value("pagination_number")
events = history.get(int(object_id))
# Events is None if object wasn't found
if events is None:
messages.error(request, _("Nonexistent entry."))
return redirect(
reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
# Add the paginator in case there are many results
events = re2o_paginator(request, events, max_result)
# Add a title in case the object was deleted
title = instance or "{} ({})".format(history.name, _("Deleted"))
return render(
request,
"logs/detailed_history.html",
{"title": title, "events": events, "related_history": history.related},
)
@login_required @login_required
def history(request, application, object_name, object_id): def history(request, application, object_name, object_id):
"""Render history for a model. """Render history for a model.
The model is determined using the `HISTORY_BIND` dictionnary if none is The model is determined using the `HISTORY_BIND` dictionnary if none is
found, raises a Http404. The view checks if the user is allowed to see the found, raises a Http404. The view checks if the user is allowed to see the
history using the `can_view` method of the model. history using the `can_view` method of the model, or the generic
Permissions are handled by get_history_object. `can_view_app("logs")` for deleted objects (see `get_history_object`).
Args: Args:
request: The request sent by the user. request: The request sent by the user.
@ -637,16 +578,29 @@ def history(request, application, object_name, object_id):
except LookupError: except LookupError:
raise Http404(_("No model found.")) raise Http404(_("No model found."))
can_view, instance = get_history_object(request, model, object_name, object_id) authorized, instance = get_history_object(request, model, object_name, object_id)
if not can_view: if not can_view:
return instance return instance
pagination_number = GeneralOption.get_cached_value("pagination_number") history = get_history_class(model)
reversions = Version.objects.get_for_object(instance) events = history.get(int(object_id), model)
if hasattr(instance, "linked_objects"):
for related_object in chain(instance.linked_objects()): # Events is None if object wasn't found
reversions = reversions | Version.objects.get_for_object(related_object) if events is None:
reversions = re2o_paginator(request, reversions, pagination_number) messages.error(request, _("Nonexistent entry."))
return render( return redirect(
request, "re2o/history.html", {"reversions": reversions, "object": instance} reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
# Generate the pagination with the objects
max_result = GeneralOption.get_cached_value("pagination_number")
events = re2o_paginator(request, events, max_result)
# Add a default title in case the object was deleted
title = instance or "{} ({})".format(history.name, _("Deleted"))
return render(
request,
"logs/detailed_history.html",
{"title": title, "events": events, "related_history": history.related},
) )

View file

@ -67,7 +67,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Create an interface" as tr_create_an_interface %} {% trans "Create an interface" as tr_create_an_interface %}
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %} {% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %}
{% acl_end %} {% acl_end %}
{% history_button machine detailed=True %} {% history_button machine %}
{% can_delete machine %} {% can_delete machine %}
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %} {% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
{% acl_end %} {% acl_end %}
@ -161,7 +161,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% acl_end %} {% acl_end %}
</ul> </ul>
</div> </div>
{% history_button interface detailed=True %} {% history_button interface %}
{% can_delete interface %} {% can_delete interface %}
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %} {% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
{% acl_end %} {% acl_end %}

View file

@ -24,13 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load i18n %} {% load i18n %}
{% if detailed %}
<a {% if class%}class="btn btn-info btn-sm"{% endif %} role="button" title="{% trans "History" %}" href="{% url 'logs:detailed-history' name id %}">
<i class="fa fa-history"></i> {% if text %}{% trans "History" %}{% endif %}
</a>
{% else %}
<a {% if class%}class="btn btn-info btn-sm"{% endif %} role="button" title="{% trans "History" %}" href="{% url 'logs:history' application name id %}"> <a {% if class%}class="btn btn-info btn-sm"{% endif %} role="button" title="{% trans "History" %}" href="{% url 'logs:history' application name id %}">
<i class="fa fa-history"></i> {% if text %}{% trans "History" %}{% endif %} <i class="fa fa-history"></i> {% if text %}{% trans "History" %}{% endif %}
</a> </a>
{% endif %}

View file

@ -176,7 +176,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Edit the groups" %} {% trans "Edit the groups" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% history_button users text=True detailed=True %} {% history_button users text=True %}
</ul> </ul>
</div> </div>
<div class="panel-body"> <div class="panel-body">