mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-23 11:53:12 +00:00
Start implementing detailed history for event class
This commit is contained in:
parent
147ff29fb5
commit
c43c532822
8 changed files with 83 additions and 98 deletions
|
@ -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"]()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
104
logs/views.py
104
logs/views.py
|
@ -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 redirect(
|
||||||
|
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(
|
return render(
|
||||||
request, "re2o/history.html", {"reversions": reversions, "object": instance}
|
request,
|
||||||
|
"logs/detailed_history.html",
|
||||||
|
{"title": title, "events": events, "related_history": history.related},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue