diff --git a/logs/models.py b/logs/models.py
index acf8771b..a9aaa7aa 100644
--- a/logs/models.py
+++ b/logs/models.py
@@ -92,6 +92,10 @@ class ActionsSearch:
return classes
+############################
+# Machine history search #
+############################
+
class MachineHistorySearchEvent:
def __init__(self, user, machine, interface, start=None, end=None):
"""
@@ -280,16 +284,25 @@ class MachineHistorySearch:
return self.events
+############################
+# Generic history classes #
+############################
+
class RelatedHistory:
- def __init__(self, name, model_name, object_id):
+ def __init__(self, version):
"""
: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.model_name = model_name
- self.object_id = object_id
+ self.version = version
+ self.app_name = version.content_type.app_label
+ 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):
return (
@@ -380,6 +393,7 @@ class History:
if self._last_version is None:
return None
+ self.name = self._last_version.object_repr
return self.events[::-1]
def _compute_diff(self, v1, v2, ignoring=[]):
@@ -417,6 +431,10 @@ class History:
self._last_version = version
+############################
+# Revision history #
+############################
+
class VersionAction(HistoryEvent):
def __init__(self, version):
self.version = version
@@ -496,6 +514,10 @@ class RevisionAction:
return self.revision.get_comment()
+############################
+# Class-specific history #
+############################
+
class UserHistoryEvent(HistoryEvent):
def _repr(self, name, value):
"""
@@ -588,7 +610,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
@@ -624,17 +646,14 @@ class UserHistory(History):
if obj is 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
self.related = (
- Version.objects.get_for_model(Machine)
+ Version.objects.all()
.filter(serialized_data__contains='"user": {}'.format(user_id))
- .order_by("-revision__date_created")
+ .order_by("content_type__model")
)
- self.related = [RelatedHistory(
- m.field_dict["name"] or _("None"),
- "machine",
- m.field_dict["id"]) for m in self.related]
+ self.related = [RelatedHistory(v) for v in self.related]
self.related = list(dict.fromkeys(self.related))
# Get all the versions for this user, with the oldest first
@@ -716,28 +735,18 @@ class MachineHistory(History):
super(MachineHistory, self).__init__()
self.event_type = MachineHistoryEvent
- def get(self, machine_id):
- # Add as "related" histories the list of Interface objects
- # that were once assigned to this machine
- self.related = list(
+ def get(self, machine_id, model):
+ self.related = (
Version.objects.get_for_model(Interface)
.filter(serialized_data__contains='"machine": {}'.format(machine_id))
- .order_by("-revision__date_created")
+ .order_by("content_type__model")
)
# Create RelatedHistory objects and remove duplicates
- self.related = [RelatedHistory(
- i.field_dict["mac_address"] or _("None"),
- "interface",
- i.field_dict["id"]) for i in self.related]
+ self.related = [RelatedHistory(v) for v in self.related]
self.related = list(dict.fromkeys(self.related))
- events = super(MachineHistory, self).get(machine_id, Machine)
-
- # Update name
- self.name = self._last_version.field_dict["name"]
-
- return events
+ return super(MachineHistory, self).get(machine_id, Machine)
class InterfaceHistoryEvent(HistoryEvent):
@@ -782,10 +791,29 @@ class InterfaceHistory(History):
super(InterfaceHistory, self).__init__()
self.event_type = InterfaceHistoryEvent
- def get(self, interface_id):
- events = super(InterfaceHistory, self).get(interface_id, Interface)
+ def get(self, interface_id, model):
+ 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"]()
diff --git a/logs/templates/logs/detailed_history.html b/logs/templates/logs/detailed_history.html
deleted file mode 100644
index 7aaf67a0..00000000
--- a/logs/templates/logs/detailed_history.html
+++ /dev/null
@@ -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 %}
-
{% blocktrans %}History of {{ title }}{% endblocktrans %}
-
-{% if events %}
-
-
-
- {% trans "Date" %} |
- {% trans "Performed by" %} |
- {% trans "Edited" %} |
- {% trans "Comment" %} |
-
-
- {% for event in events %}
-
- {{ event.date }} |
-
- {% if event.performed_by %}
-
- {{ event.performed_by }}
-
- {% else %}
- {% trans "Unknown" %}
- {% endif %}
- |
-
- {% for edit in event.edits %}
- {% if edit.1 is None and edit.2 is None %}
- {{ edit.0 }}
- {% elif edit.1 is None %}
- {{ edit.0 }}:
- {{ edit.2 }}
- {% else %}
- {{ edit.0 }}:
- {{ edit.1 }}
- ➔ {{ edit.2 }}
- {% endif %}
- {% endfor %}
- |
- {{ event.comment }} |
-
- {% endfor %}
-
- {% include 'pagination.html' with list=events %}
-{% else %}
- {% trans "No event" %}
-{% endif %}
-
-{% if related_history %}
-
- {% blocktrans %}Related elements{% endblocktrans %}
-
-
-{% endif %}
-
-
-
-
-{% endblock %}
diff --git a/logs/templatetags/logs_extra.py b/logs/templatetags/logs_extra.py
index 2e58cb67..c436c1fa 100644
--- a/logs/templatetags/logs_extra.py
+++ b/logs/templatetags/logs_extra.py
@@ -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,
}
diff --git a/logs/urls.py b/logs/urls.py
index 914761bf..d70cc4a8 100644
--- a/logs/urls.py
+++ b/logs/urls.py
@@ -42,11 +42,6 @@ urlpatterns = [
views.history,
name="history",
),
- url(
- r"(?P\w+)/(?P[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"),
diff --git a/logs/views.py b/logs/views.py
index b71187aa..3f9fac5e 100644
--- a/logs/views.py
+++ b/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,
+ "re2o/history.html",
+ {"title": title, "events": events, "related_history": history.related},
)
diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html
index 857d2a3a..77b65546 100644
--- a/machines/templates/machines/aff_machines.html
+++ b/machines/templates/machines/aff_machines.html
@@ -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 %}
- {% 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 %}
diff --git a/re2o/templates/re2o/aff_history.html b/re2o/templates/re2o/aff_history.html
index 6256ff23..a52ecc38 100644
--- a/re2o/templates/re2o/aff_history.html
+++ b/re2o/templates/re2o/aff_history.html
@@ -6,6 +6,7 @@ quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Lara Kermarec
Copyright © 2017 Augustin Lemesle
+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
@@ -23,36 +24,73 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load i18n %}
+{% load logs_extra %}
- {% if reversions.paginator %}
-
- {% endif %}
-
-
-
+{% if events %}
+
+
+
+ {% trans "Date" %} |
+ {% trans "Performed by" %} |
+ {% trans "Edited" %} |
+ {% trans "Comment" %} |
+
+
+ {% for event in events %}
- {% trans "Date" %} |
- {% trans "Performed by" %} |
- {% trans "Comment" %} |
+ {{ event.date }} |
+
+ {% if event.performed_by %}
+
+ {{ event.performed_by }}
+
+ {% else %}
+ {% trans "Unknown" %}
+ {% endif %}
+ |
+
+ {% for edit in event.edits %}
+ {% if edit.1 is None and edit.2 is None %}
+ {{ edit.0 }}
+ {% elif edit.1 is None %}
+ {{ edit.0 }}:
+ {{ edit.2 }}
+ {% else %}
+ {{ edit.0 }}:
+ {{ edit.1 }}
+ ➔ {{ edit.2 }}
+ {% endif %}
+ {% endfor %}
+ |
+ {{ event.comment }} |
-
- {% for rev in reversions %}
-
- {{ rev.revision.date_created }} |
- {{ rev.revision.user }} |
- {{ rev.revision.comment }} |
-
- {% endfor %}
-
+ {% endfor %}
+
+ {% include 'pagination.html' with list=events %}
+{% else %}
+ {% trans "No event" %}
+{% endif %}
+
+{% if related_history %}
+
+ {% blocktrans %}Related elements{% endblocktrans %}
+
+
+ {% for related in related_history %}
+ -
+ {% if related.object_id %}
+
+ {{ related.name }}
+
+ {% else %}
+ {{ related.name }}
+ {% endif %}
+
+ {% endfor %}
+
+{% endif %}
+
+
+
+
diff --git a/re2o/templates/re2o/history.html b/re2o/templates/re2o/history.html
index 285fd62f..53ccd587 100644
--- a/re2o/templates/re2o/history.html
+++ b/re2o/templates/re2o/history.html
@@ -7,6 +7,7 @@ quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Lara Kermarec
Copyright © 2017 Augustin Lemesle
+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
@@ -29,8 +30,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}{% trans "History" %}{% endblock %}
{% block content %}
-{% blocktrans %}History of {{ object }}{% endblocktrans %}
- {% include 're2o/aff_history.html' with reversions=reversions %}
+{% blocktrans %}History of {{ title }}{% endblocktrans %}
+ {% include 're2o/aff_history.html' with events=events related_history=related_history %}
diff --git a/templates/buttons/history.html b/templates/buttons/history.html
index 2495dec8..71c2c71d 100644
--- a/templates/buttons/history.html
+++ b/templates/buttons/history.html
@@ -24,13 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load i18n %}
-{% if detailed %}
-
- {% if text %}{% trans "History" %}{% endif %}
-
-{% else %}
{% if text %}{% trans "History" %}{% endif %}
-{% endif %}
diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html
index 4fa780a7..4fec3c18 100644
--- a/users/templates/users/profil.html
+++ b/users/templates/users/profil.html
@@ -176,7 +176,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Edit the groups" %}
{% acl_end %}
- {% history_button users text=True detailed=True %}
+ {% history_button users text=True %}