From d06d6f8c00bbe9520c99e48ca938cd3ac52a3f97 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Thu, 23 Apr 2020 13:18:16 +0200 Subject: [PATCH] Create specific view for user history --- logs/models.py | 120 ++++++++++++++++++++++++-- logs/templates/logs/user_history.html | 71 +++++++++++++++ logs/urls.py | 1 + logs/views.py | 22 ++++- 4 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 logs/templates/logs/user_history.html diff --git a/logs/models.py b/logs/models.py index 343a33cb..01927de8 100644 --- a/logs/models.py +++ b/logs/models.py @@ -29,7 +29,7 @@ from machines.models import Machine from users.models import User -class HistoryEvent: +class MachineHistoryEvent: def __init__(self, user, machine, interface, start=None, end=None): """ :param user: User, The user owning the maching at the time of the event @@ -80,7 +80,7 @@ class MachineHistory: """ :param search: ip or mac to lookup :param params: dict built by the search view - :return: list or None, a list of HistoryEvent + :return: list or None, a list of MachineHistoryEvent """ self.start = params.get("s", None) self.end = params.get("e", None) @@ -101,7 +101,7 @@ class MachineHistory: :param machine: Version, the machine version related to the interface :param interface: Version, the interface targeted by this event """ - evt = HistoryEvent(user, machine, interface) + evt = MachineHistoryEvent(user, machine, interface) evt.start_date = interface.revision.date_created # Try not to recreate events if it's unnecessary @@ -177,7 +177,7 @@ class MachineHistory: def __get_by_ip(self, ip): """ :param ip: str, The IP to lookup - :returns: list, a list of HistoryEvent + :returns: list, a list of MachineHistoryEvent """ interfaces = self.__get_interfaces_for_ip(ip) @@ -193,7 +193,7 @@ class MachineHistory: def __get_by_mac(self, mac): """ :param mac: str, The MAC address to lookup - :returns: list, a list of HistoryEvent + :returns: list, a list of MachineHistoryEvent """ interfaces = self.__get_interfaces_for_mac(mac) @@ -205,3 +205,113 @@ class MachineHistory: self.__add_revision(user, machine, interface) return self.events + + +class UserHistoryEvent: + def __init__(self, user, version, previous_version=None, edited_fields=None): + """ + :param user: User, The user who's history is being built + :param version: Version, the version of the user for this event + :param previous_version: Version, the version of the user before this event + :param edited_fields: list, The list of modified fields by this event + """ + self.user = user + self.version = version + self.previous_version = previous_version + self.edited_fields = edited_fields + self.date = version.revision.date_created + self.performed_by = version.revision.user + self.comment = version.revision.get_comment() or None + + def edits(self, hide=["password", "pwd_ntlm"]): + """ + Build a list of the changes performed during this event + :param hide: list, the list of fields for which not to show details + :return: str + """ + edits = [] + + for field in self.edited_fields: + if field in hide: + # Don't show sensitive information + edits.append((field, None, None)) + else: + edits.append(( + field, + self.previous_version.field_dict[field], + self.version.field_dict[field] + )) + + return edits + + def __repr__(self): + return "{} edited fields {} of {} ({})".format( + self.performed_by, + self.edited_fields or "nothing", + self.user, + self.comment or "No comment" + ) + + +class UserHistory: + def __init__(self): + self.events = [] + self.user = None + self.__last_version = None + + def get(self, user_id): + """ + :param user_id: id of the user to lookup + :return: list or None, a list of UserHistoryEvent, in reverse chronological order + """ + self.events = [] + try: + self.user = User.objects.get(id=user_id) + except User.DoesNotExist: + return None + + # Get all the versions for this user, with the oldest first + user_versions = filter( + lambda x: x.field_dict["id"] == user_id, + Version.objects.get_for_model(User).order_by("revision__date_created") + ) + + for version in user_versions: + self.__add_revision(self.user, version) + + return self.events[::-1] + + def __compute_diff(self, v1, v2, ignoring=["last_login", "comment", "pwd_ntlm", "email_change_date"]): + """ + Find the edited field between two versions + :param v1: Version + :param v2: Version + :param ignoring: List, a list of fields to ignore + :return: List of field names + """ + fields = [] + + for key in v1.field_dict.keys(): + if key not in ignoring and v1.field_dict[key] != v2.field_dict[key]: + fields.append(key) + + return fields + + def __add_revision(self, user, version): + """ + Add a new revision to the chronological order + :param user: User, The user displayed in this history + :param version: Version, The version of the user for this event + """ + diff = None + if self.__last_version is not None: + diff = self.__compute_diff(version, self.__last_version) + + # Ignore "empty" events like login + if not diff: + self.__last_version = version + return + + evt = UserHistoryEvent(user, version, self.__last_version, diff) + self.events.append(evt) + self.__last_version = version diff --git a/logs/templates/logs/user_history.html b/logs/templates/logs/user_history.html new file mode 100644 index 00000000..6cf86625 --- /dev/null +++ b/logs/templates/logs/user_history.html @@ -0,0 +1,71 @@ +{% 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 %} + +{% block title %}{% trans "History" %}{% endblock %} + +{% block content %} +

{% blocktrans %}History of {{ user }}{% endblocktrans %}

+ +{% if events %} + + + + + + + + + + {% for event in events %} + + + + + + + {% endfor %} +
{% trans "Performed by" %}{% trans "Date" %}{% trans "Diff" %}{% trans "Comment" %}
+ + {{ event.performed_by }} + + {{ event.date }} + {% for edit in event.edits %} + {% if edit[1] and edit[2] %} +

{{ edit[0] }}: {{ edit[1] }} ➔ {{ edit[2] }}

+ {% elif edit[2] %} +

{{ edit[0] }}: {{ edit[2] }}

+ {% else %} +

{{ edit[0] }}

+ {% endif %} + {% endfor %} +
{{ event.comment }}
+ {% include 'pagination.html' with list=events %} +{% else %} +

{% trans "No event" %}

+{% endif %} +
+
+
diff --git a/logs/urls.py b/logs/urls.py index eced2a83..adde5d3c 100644 --- a/logs/urls.py +++ b/logs/urls.py @@ -47,4 +47,5 @@ urlpatterns = [ name="history", ), url(r"^stats_search_machine/$", views.stats_search_machine_history, name="stats-search-machine"), + url(r"^user/(?P[0-9]+)$", views.user_history, name="stats-user-history"), ] diff --git a/logs/views.py b/logs/views.py index 85ba35cf..a36e90cb 100644 --- a/logs/views.py +++ b/logs/views.py @@ -101,7 +101,7 @@ from re2o.utils import ( from re2o.base import re2o_paginator, SortTable from re2o.acl import can_view_all, can_view_app, can_edit_history -from .models import MachineHistory +from .models import MachineHistory, UserHistory from .forms import MachineHistoryForm @@ -508,6 +508,26 @@ def stats_search_machine_history(request): return render(request, "logs/search_machine_history.html", {"history_form": history_form}) +@login_required +@can_view_app("users") +def user_history(request, user_id): + history = UserHistory() + events = history.get(user_id) + + max_result = GeneralOption.get_cached_value("pagination_number") + events = re2o_paginator( + request, + events, + max_result + ) + + return render( + request, + "logs/user_history.html", + { "user": history.user, "events": events }, + ) + + def history(request, application, object_name, object_id): """Render history for a model.