mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-22 15:03:45 +00:00
Start implementing detailed history for event class
This commit is contained in:
parent
63f09d7867
commit
4190f4f39e
8 changed files with 83 additions and 98 deletions
|
@ -92,6 +92,10 @@ class ActionsSearch:
|
|||
return classes
|
||||
|
||||
|
||||
############################
|
||||
# Machine history search #
|
||||
############################
|
||||
|
||||
class MachineHistorySearchEvent:
|
||||
def __init__(self, user, machine, interface, start=None, end=None):
|
||||
"""
|
||||
|
@ -280,14 +284,19 @@ class MachineHistorySearch:
|
|||
return self.events
|
||||
|
||||
|
||||
############################
|
||||
# Generic history classes #
|
||||
############################
|
||||
|
||||
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 model_name: Name of the related model (e.g. "user")
|
||||
:param object_id: ID of the related object
|
||||
"""
|
||||
self.name = "{} (id = {})".format(name, object_id)
|
||||
self.app_name = app_name
|
||||
self.model_name = model_name
|
||||
self.object_id = object_id
|
||||
|
||||
|
@ -417,6 +426,10 @@ class History:
|
|||
self._last_version = version
|
||||
|
||||
|
||||
############################
|
||||
# Revision history #
|
||||
############################
|
||||
|
||||
class VersionAction(HistoryEvent):
|
||||
def __init__(self, version):
|
||||
self.version = version
|
||||
|
@ -496,6 +509,10 @@ class RevisionAction:
|
|||
return self.revision.get_comment()
|
||||
|
||||
|
||||
############################
|
||||
# Class-specific history #
|
||||
############################
|
||||
|
||||
class UserHistoryEvent(HistoryEvent):
|
||||
def _repr(self, name, value):
|
||||
"""
|
||||
|
@ -588,7 +605,7 @@ class UserHistory(History):
|
|||
super(UserHistory, self).__init__()
|
||||
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
|
||||
:return: list or None, a list of UserHistoryEvent, in reverse chronological order
|
||||
|
@ -633,7 +650,8 @@ class UserHistory(History):
|
|||
)
|
||||
self.related = [RelatedHistory(
|
||||
m.field_dict["name"] or _("None"),
|
||||
"machine",
|
||||
"machines"
|
||||
"Machine",
|
||||
m.field_dict["id"]) for m in self.related]
|
||||
self.related = list(dict.fromkeys(self.related))
|
||||
|
||||
|
@ -716,7 +734,7 @@ class MachineHistory(History):
|
|||
super(MachineHistory, self).__init__()
|
||||
self.event_type = MachineHistoryEvent
|
||||
|
||||
def get(self, machine_id):
|
||||
def get(self, machine_id, model):
|
||||
# Add as "related" histories the list of Interface objects
|
||||
# that were once assigned to this machine
|
||||
self.related = list(
|
||||
|
@ -728,7 +746,8 @@ class MachineHistory(History):
|
|||
# Create RelatedHistory objects and remove duplicates
|
||||
self.related = [RelatedHistory(
|
||||
i.field_dict["mac_address"] or _("None"),
|
||||
"interface",
|
||||
"machines",
|
||||
"Interface",
|
||||
i.field_dict["id"]) for i in self.related]
|
||||
self.related = list(dict.fromkeys(self.related))
|
||||
|
||||
|
@ -782,10 +801,34 @@ class InterfaceHistory(History):
|
|||
super(InterfaceHistory, self).__init__()
|
||||
self.event_type = InterfaceHistoryEvent
|
||||
|
||||
def get(self, interface_id):
|
||||
def get(self, interface_id, model):
|
||||
events = super(InterfaceHistory, self).get(interface_id, Interface)
|
||||
|
||||
# Update name
|
||||
self.name = self._last_version.field_dict["mac_address"]
|
||||
|
||||
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>
|
||||
{% for related in related_history %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
@ -42,7 +42,7 @@ def is_facture(baseinvoice):
|
|||
|
||||
|
||||
@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.
|
||||
|
||||
Args:
|
||||
|
@ -57,6 +57,5 @@ def history_button(instance, text=False, detailed=False, html_class=True):
|
|||
"name": instance._meta.model_name,
|
||||
"id": instance.id,
|
||||
"text": text,
|
||||
"detailed": detailed,
|
||||
"class": html_class,
|
||||
}
|
||||
|
|
|
@ -42,11 +42,6 @@ urlpatterns = [
|
|||
views.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_models/$", views.stats_models, name="stats-models"),
|
||||
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 itertools import chain
|
||||
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import render, redirect
|
||||
|
@ -105,9 +104,7 @@ from .models import (
|
|||
ActionsSearch,
|
||||
RevisionAction,
|
||||
MachineHistorySearch,
|
||||
UserHistory,
|
||||
MachineHistory,
|
||||
InterfaceHistory
|
||||
get_history_class
|
||||
)
|
||||
|
||||
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})
|
||||
|
||||
|
||||
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
|
||||
Handles permissions and DoesNotExist errors
|
||||
"""
|
||||
is_deleted = False
|
||||
|
||||
try:
|
||||
object_name_id = object_name + "id"
|
||||
kwargs = {object_name_id: object_id}
|
||||
instance = model.get_instance(**kwargs)
|
||||
except model.DoesNotExist:
|
||||
is_deleted = True
|
||||
instance = None
|
||||
|
||||
if is_deleted and not allow_deleted:
|
||||
messages.error(request, _("Nonexistent entry."))
|
||||
return False, redirect(
|
||||
reverse("users:profil", kwargs={"userid": str(request.user.id)})
|
||||
)
|
||||
|
||||
if is_deleted:
|
||||
can_view = can_view_app("logs")
|
||||
if instance is None:
|
||||
authorized = can_view_app("logs")
|
||||
msg = None
|
||||
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(
|
||||
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
|
||||
|
||||
|
||||
@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
|
||||
def history(request, application, object_name, object_id):
|
||||
"""Render history for a model.
|
||||
|
||||
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
|
||||
history using the `can_view` method of the model.
|
||||
Permissions are handled by get_history_object.
|
||||
history using the `can_view` method of the model, or the generic
|
||||
`can_view_app("logs")` for deleted objects (see `get_history_object`).
|
||||
|
||||
Args:
|
||||
request: The request sent by the user.
|
||||
|
@ -637,16 +578,29 @@ def history(request, application, object_name, object_id):
|
|||
except LookupError:
|
||||
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:
|
||||
return instance
|
||||
|
||||
pagination_number = GeneralOption.get_cached_value("pagination_number")
|
||||
reversions = Version.objects.get_for_object(instance)
|
||||
if hasattr(instance, "linked_objects"):
|
||||
for related_object in chain(instance.linked_objects()):
|
||||
reversions = reversions | Version.objects.get_for_object(related_object)
|
||||
reversions = re2o_paginator(request, reversions, pagination_number)
|
||||
history = get_history_class(model)
|
||||
events = history.get(int(object_id), model)
|
||||
|
||||
# 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)})
|
||||
)
|
||||
|
||||
# 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, "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 %}
|
||||
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %}
|
||||
{% acl_end %}
|
||||
{% history_button machine detailed=True %}
|
||||
{% history_button machine %}
|
||||
{% can_delete machine %}
|
||||
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
|
||||
{% acl_end %}
|
||||
|
@ -161,7 +161,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% acl_end %}
|
||||
</ul>
|
||||
</div>
|
||||
{% history_button interface detailed=True %}
|
||||
{% history_button interface %}
|
||||
{% can_delete interface %}
|
||||
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
|
||||
{% acl_end %}
|
||||
|
|
|
@ -24,13 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
{% 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 %}">
|
||||
<i class="fa fa-history"></i> {% if text %}{% trans "History" %}{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% trans "Edit the groups" %}
|
||||
</a>
|
||||
{% acl_end %}
|
||||
{% history_button users text=True detailed=True %}
|
||||
{% history_button users text=True %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
|
Loading…
Reference in a new issue