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 %} - - - - - - - - - - {% for event in events %} - - - - - - - {% endfor %} -
{% trans "Date" %}{% trans "Performed by" %}{% trans "Edited" %}{% 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 }}
- {% 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 %} +
+ + + + + + + + + {% for event in events %} - - - + + + + - - {% for rev in reversions %} - - - - - - {% endfor %} -
{% trans "Date" %}{% trans "Performed by" %}{% trans "Edited" %}{% trans "Comment" %}
{% 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 }}
{{ rev.revision.date_created }}{{ rev.revision.user }}{{ rev.revision.comment }}
+ {% endfor %} + + {% include 'pagination.html' with list=events %} +{% else %} +

{% trans "No event" %}

+{% endif %} + +{% if related_history %} + +

{% blocktrans %}Related elements{% endblocktrans %}

+ + +{% 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 %}