8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-23 15:33:45 +00:00

Merge branch 'complete_detailed_history' into 'dev'

Finish replacing old history view

See merge request re2o/re2o!522
This commit is contained in:
chirac 2020-04-25 13:39:32 +02:00
commit 29b36bb346
10 changed files with 163 additions and 248 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,16 +284,25 @@ 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, version):
""" """
: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.version = version
self.model_name = model_name self.app_name = version.content_type.app_label
self.object_id = object_id self.model_name = version.content_type.model
self.object_id = version.object_id
self.name = version.object_repr
if self.model_name:
self.name = "{}: {}".format(self.model_name.title(), self.name)
def __eq__(self, other): def __eq__(self, other):
return ( return (
@ -380,6 +393,7 @@ class History:
if self._last_version is None: if self._last_version is None:
return None return None
self.name = self._last_version.object_repr
return self.events[::-1] return self.events[::-1]
def _compute_diff(self, v1, v2, ignoring=[]): def _compute_diff(self, v1, v2, ignoring=[]):
@ -417,6 +431,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 +514,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 +610,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
@ -624,17 +646,14 @@ class UserHistory(History):
if obj is None: if obj is None:
return None return None
# Add in "related" elements the list of Machine objects # Add in "related" elements the list of objects
# that were once owned by this user # that were once owned by this user
self.related = ( self.related = (
Version.objects.get_for_model(Machine) Version.objects.all()
.filter(serialized_data__contains='"user": {}'.format(user_id)) .filter(serialized_data__contains='"user": {}'.format(user_id))
.order_by("-revision__date_created") .order_by("content_type__model")
) )
self.related = [RelatedHistory( self.related = [RelatedHistory(v) for v in self.related]
m.field_dict["name"] or _("None"),
"machine",
m.field_dict["id"]) for m in self.related]
self.related = list(dict.fromkeys(self.related)) self.related = list(dict.fromkeys(self.related))
# Get all the versions for this user, with the oldest first # Get all the versions for this user, with the oldest first
@ -716,28 +735,18 @@ 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 self.related = (
# that were once assigned to this machine
self.related = list(
Version.objects.get_for_model(Interface) Version.objects.get_for_model(Interface)
.filter(serialized_data__contains='"machine": {}'.format(machine_id)) .filter(serialized_data__contains='"machine": {}'.format(machine_id))
.order_by("-revision__date_created") .order_by("content_type__model")
) )
# Create RelatedHistory objects and remove duplicates # Create RelatedHistory objects and remove duplicates
self.related = [RelatedHistory( self.related = [RelatedHistory(v) for v in self.related]
i.field_dict["mac_address"] or _("None"),
"interface",
i.field_dict["id"]) for i in self.related]
self.related = list(dict.fromkeys(self.related)) self.related = list(dict.fromkeys(self.related))
events = super(MachineHistory, self).get(machine_id, Machine) return super(MachineHistory, self).get(machine_id, Machine)
# Update name
self.name = self._last_version.field_dict["name"]
return events
class InterfaceHistoryEvent(HistoryEvent): class InterfaceHistoryEvent(HistoryEvent):
@ -782,10 +791,29 @@ 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) return 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"]()

View file

@ -1,94 +0,0 @@
{% extends 'logs/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2020 Jean-Romain Garnier
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load i18n %}
{% load logs_extra %}
{% block title %}{% trans "History" %}{% endblock %}
{% block content %}
<h2>{% blocktrans %}History of {{ title }}{% endblocktrans %}</h2>
{% if events %}
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Performed by" %}</th>
<th>{% trans "Edited" %}</th>
<th>{% trans "Comment" %}</th>
</tr>
</thead>
{% for event in events %}
<tr>
<td>{{ event.date }}</td>
<td>
{% if event.performed_by %}
<a href="{% url 'users:profil' userid=event.performed_by.id %}" title=tr_view_the_profile>
{{ event.performed_by }}
</a>
{% else %}
{% trans "Unknown" %}
{% endif %}
</td>
<td>
{% for edit in event.edits %}
{% if edit.1 is None and edit.2 is None %}
<strong>{{ edit.0 }}</strong><br/>
{% elif edit.1 is None %}
<strong>{{ edit.0 }}:</strong>
<i class="text-success"> {{ edit.2 }}</i><br/>
{% else %}
<strong>{{ edit.0 }}:</strong>
<i class="text-danger"> {{ edit.1 }} </i>
<i class="text-success">{{ edit.2 }}</i><br/>
{% endif %}
{% endfor %}
</td>
<td>{{ event.comment }}</td>
</tr>
{% endfor %}
</table>
{% include 'pagination.html' with list=events %}
{% else %}
<h3>{% trans "No event" %}</h3>
{% endif %}
{% if related_history %}
<h2>{% blocktrans %}Related elements{% endblocktrans %}</h2>
<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>
</li>
{% endfor %}
</ul>
{% endif %}
<br />
<br />
<br />
{% endblock %}

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,
"re2o/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

@ -6,6 +6,7 @@ quelques clics.
Copyright © 2017 Gabriel Détraz Copyright © 2017 Gabriel Détraz
Copyright © 2017 Lara Kermarec Copyright © 2017 Lara Kermarec
Copyright © 2017 Augustin Lemesle Copyright © 2017 Augustin Lemesle
Copyright © 2020 Jean-Romain Garnier
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -23,36 +24,73 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load i18n %} {% load i18n %}
{% load logs_extra %}
{% if reversions.paginator %} {% if events %}
<ul class="pagination nav navbar-nav"> <table class="table table-striped">
{% if reversions.has_previous %}
<li><a href="?page={{ reversions.previous_page_number }}">{% trans "Next" %}</a></li>
{% endif %}
{% for page in reversions.paginator.page_range %}
<li class="{% if reversions.number == page %}active{% endif %}"><a href="?page={{page }}">{{ page }}</a></li>
{% endfor %}
{% if reversions.has_next %}
<li> <a href="?page={{ reversions.next_page_number }}">{% trans "Previous" %}</a></li>
{% endif %}
</ul>
{% endif %}
<table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% trans "Date" %}</th> <th>{% trans "Date" %}</th>
<th>{% trans "Performed by" %}</th> <th>{% trans "Performed by" %}</th>
<th>{% trans "Edited" %}</th>
<th>{% trans "Comment" %}</th> <th>{% trans "Comment" %}</th>
</tr> </tr>
</thead> </thead>
{% for rev in reversions %} {% for event in events %}
<tr> <tr>
<td>{{ rev.revision.date_created }}</td> <td>{{ event.date }}</td>
<td>{{ rev.revision.user }}</td> <td>
<td>{{ rev.revision.comment }}</td> {% if event.performed_by %}
<a href="{% url 'users:profil' userid=event.performed_by.id %}" title=tr_view_the_profile>
{{ event.performed_by }}
</a>
{% else %}
{% trans "Unknown" %}
{% endif %}
</td>
<td>
{% for edit in event.edits %}
{% if edit.1 is None and edit.2 is None %}
<strong>{{ edit.0 }}</strong><br/>
{% elif edit.1 is None %}
<strong>{{ edit.0 }}:</strong>
<i class="text-success"> {{ edit.2 }}</i><br/>
{% else %}
<strong>{{ edit.0 }}:</strong>
<i class="text-danger"> {{ edit.1 }} </i>
<i class="text-success">{{ edit.2 }}</i><br/>
{% endif %}
{% endfor %}
</td>
<td>{{ event.comment }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% include 'pagination.html' with list=events %}
{% else %}
<h3>{% trans "No event" %}</h3>
{% endif %}
{% if related_history %}
<h2>{% blocktrans %}Related elements{% endblocktrans %}</h2>
<ul>
{% for related in related_history %}
<li>
{% if related.object_id %}
<a href="{% url 'logs:history' related.app_name related.model_name related.object_id %}" title="{% trans "History" %}">
{{ related.name }}
</a>
{% else %}
{{ related.name }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<br />
<br />
<br />

View file

@ -7,6 +7,7 @@ quelques clics.
Copyright © 2017 Gabriel Détraz Copyright © 2017 Gabriel Détraz
Copyright © 2017 Lara Kermarec Copyright © 2017 Lara Kermarec
Copyright © 2017 Augustin Lemesle Copyright © 2017 Augustin Lemesle
Copyright © 2020 Jean-Romain Garnier
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -29,8 +30,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "History" %}{% endblock %} {% block title %}{% trans "History" %}{% endblock %}
{% block content %} {% block content %}
<h2>{% blocktrans %}History of {{ object }}{% endblocktrans %}</h2> <h2>{% blocktrans %}History of {{ title }}{% endblocktrans %}</h2>
{% include 're2o/aff_history.html' with reversions=reversions %} {% include 're2o/aff_history.html' with events=events related_history=related_history %}
<br /> <br />
<br /> <br />
<br /> <br />

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">