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

Merge branch 'machine_interface_history' into 'dev'

Add detailed history for machine and interface

See merge request re2o/re2o!517
This commit is contained in:
klafyvel 2020-04-23 22:06:49 +02:00
commit e7795a775c
21 changed files with 756 additions and 434 deletions

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2019-01-07 01:37+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language: fr_FR\n"

View file

@ -18,7 +18,7 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""The forms used by the search app"""
"""The forms used by the machine search view"""
from django import forms
from django.forms import Form
@ -31,7 +31,7 @@ CHOICES_TYPE = (
)
class MachineHistoryForm(Form):
class MachineHistorySearchForm(Form):
"""The form for a simple search"""
q = forms.CharField(
@ -46,7 +46,7 @@ class MachineHistoryForm(Form):
e = forms.DateField(required=False, label=_("End date"))
def __init__(self, *args, **kwargs):
super(MachineHistoryForm, self).__init__(*args, **kwargs)
super(MachineHistorySearchForm, self).__init__(*args, **kwargs)
self.fields["s"].help_text = get_input_formats_help_text(
self.fields["s"].input_formats
)

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-06-23 16:01+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -58,20 +58,27 @@ msgstr "Date de début"
msgid "End date"
msgstr "Date de fin"
#: logs/models.py:238 logs/models.py:263
#: logs/models.py:260 logs/models.py:364 logs/models.py:397 logs/models.py:480
#: logs/models.py:572 logs/models.py:610
msgid "None"
msgstr "Aucun(e)"
#: logs/models.py:248
#: logs/models.py:374 logs/models.py:393 logs/models.py:407 logs/models.py:552
#: logs/models.py:597 logs/models.py:602 logs/models.py:607 logs/models.py:617
#: logs/views.py:592
msgid "Deleted"
msgstr "Supprimé(e)"
#: logs/models.py:255 logs/models.py:260
#: logs/models.py:381 logs/models.py:386
#: logs/templates/logs/detailed_history.html:52
#: logs/templates/logs/machine_history.html:55
#: logs/templates/logs/user_history.html:51
msgid "Unknown"
msgstr "Inconnu(e)"
#: logs/models.py:605
msgid "No name"
msgstr "Sans nom"
#: logs/templates/logs/aff_stats_logs.html:36
msgid "Edited object"
msgstr "Objet modifié"
@ -90,8 +97,8 @@ msgid "Date of editing"
msgstr "Date de modification"
#: logs/templates/logs/aff_stats_logs.html:42
#: logs/templates/logs/detailed_history.html:40
#: logs/templates/logs/machine_history.html:39
#: logs/templates/logs/user_history.html:39
msgid "Comment"
msgstr "Commentaire"
@ -128,7 +135,7 @@ msgid "Rank"
msgstr "Rang"
#: logs/templates/logs/aff_summary.html:37
#: logs/templates/logs/user_history.html:36
#: logs/templates/logs/detailed_history.html:37
msgid "Date"
msgstr "Date"
@ -192,6 +199,31 @@ msgstr ""
msgid "Confirm"
msgstr "Confirmer"
#: logs/templates/logs/detailed_history.html:28
#: logs/templates/logs/detailed_history.html:85
msgid "History"
msgstr "Historique"
#: logs/templates/logs/detailed_history.html:31
msgid "History of %(title)s"
msgstr "Historique de %(title)s"
#: logs/templates/logs/detailed_history.html:38
msgid "Performed by"
msgstr "Effectué(e) par"
#: logs/templates/logs/detailed_history.html:39
msgid "Edited"
msgstr "Modifié"
#: logs/templates/logs/detailed_history.html:75
msgid "No event"
msgstr "Aucun évènement"
#: logs/templates/logs/detailed_history.html:80
msgid "Related elements"
msgstr "Élements liés"
#: logs/templates/logs/index.html:29 logs/templates/logs/stats_general.html:29
#: logs/templates/logs/stats_logs.html:29
#: logs/templates/logs/stats_models.html:29
@ -200,7 +232,7 @@ msgid "Statistics"
msgstr "Statistiques"
#: logs/templates/logs/index.html:32 logs/templates/logs/stats_logs.html:32
#: logs/views.py:421
#: logs/views.py:427
msgid "Actions performed"
msgstr "Actions effectuées"
@ -265,159 +297,138 @@ msgstr "Statistiques sur la base de données"
msgid "Statistics about users"
msgstr "Statistiques sur les utilisateurs"
#: logs/templates/logs/user_history.html:27
msgid "History"
msgstr "Historique"
#: logs/templates/logs/user_history.html:30
#, python-format
msgid "History of %(user)s"
msgstr "Historique de %(user)s"
#: logs/templates/logs/user_history.html:37
msgid "Performed by"
msgstr "Effectué(e) par"
#: logs/templates/logs/user_history.html:38
msgid "Edited"
msgstr "Modifié"
#: logs/templates/logs/user_history.html:74
msgid "No event"
msgstr "Aucun évènement"
#: logs/views.py:178
#: logs/views.py:184
msgid "Nonexistent revision."
msgstr "Révision inexistante."
#: logs/views.py:181
#: logs/views.py:187
msgid "The action was deleted."
msgstr "L'action a été supprimée."
#: logs/views.py:222
#: logs/views.py:228
msgid "Category"
msgstr "Catégorie"
#: logs/views.py:223
#: logs/views.py:229
msgid "Number of users (members and clubs)"
msgstr "Nombre d'utilisateurs (adhérents et clubs)"
#: logs/views.py:224
#: logs/views.py:230
msgid "Number of members"
msgstr "Nombre d'adhérents"
#: logs/views.py:225
#: logs/views.py:231
msgid "Number of clubs"
msgstr "Nombre de clubs"
#: logs/views.py:229
#: logs/views.py:235
msgid "Activated users"
msgstr "Utilisateurs activés"
#: logs/views.py:235
#: logs/views.py:241
msgid "Disabled users"
msgstr "Utilisateurs désactivés"
#: logs/views.py:241
#: logs/views.py:247
msgid "Archived users"
msgstr "Utilisateurs archivés"
#: logs/views.py:247
#: logs/views.py:253
msgid "Fully archived users"
msgstr "Utilisateurs complètement archivés"
#: logs/views.py:257
#: logs/views.py:263
msgid "Not yet active users"
msgstr "Utilisateurs pas encore actifs"
#: logs/views.py:267
#: logs/views.py:273
msgid "Contributing members"
msgstr "Adhérents cotisants"
#: logs/views.py:273
#: logs/views.py:279
msgid "Users benefiting from a connection"
msgstr "Utilisateurs bénéficiant d'une connexion"
#: logs/views.py:279
#: logs/views.py:285
msgid "Banned users"
msgstr "Utilisateurs bannis"
#: logs/views.py:285
#: logs/views.py:291
msgid "Users benefiting from a free connection"
msgstr "Utilisateurs bénéficiant d'une connexion gratuite"
#: logs/views.py:291
#: logs/views.py:297
msgid "Users with a confirmed email"
msgstr "Utilisateurs ayant un mail confirmé"
#: logs/views.py:297
#: logs/views.py:303
msgid "Users with an unconfirmed email"
msgstr "Utilisateurs ayant un mail non confirmé"
#: logs/views.py:303
#: logs/views.py:309
msgid "Users pending email confirmation"
msgstr "Utilisateurs en attente de confirmation du mail"
#: logs/views.py:309
#: logs/views.py:315
msgid "Active interfaces (with access to the network)"
msgstr "Interfaces actives (ayant accès au réseau)"
#: logs/views.py:323
#: logs/views.py:329
msgid "Active interfaces assigned IPv4"
msgstr "Interfaces actives assignées IPv4"
#: logs/views.py:340
#: logs/views.py:346
msgid "IP range"
msgstr "Plage d'IP"
#: logs/views.py:341
#: logs/views.py:347
msgid "VLAN"
msgstr "VLAN"
#: logs/views.py:342
#: logs/views.py:348
msgid "Total number of IP addresses"
msgstr "Nombre total d'adresses IP"
#: logs/views.py:343
#: logs/views.py:349
msgid "Number of assigned IP addresses"
msgstr "Nombre d'adresses IP assignées"
#: logs/views.py:344
#: logs/views.py:350
msgid "Number of IP address assigned to an activated machine"
msgstr "Nombre d'adresses IP assignées à une machine activée"
#: logs/views.py:345
#: logs/views.py:351
msgid "Number of unassigned IP addresses"
msgstr "Nombre d'adresses IP non assignées"
#: logs/views.py:360
#: logs/views.py:366
msgid "Users (members and clubs)"
msgstr "Utilisateurs (adhérents et clubs)"
#: logs/views.py:406
#: logs/views.py:412
msgid "Topology"
msgstr "Topologie"
#: logs/views.py:422
#: logs/views.py:428
msgid "Number of actions"
msgstr "Nombre d'actions"
#: logs/views.py:447
#: logs/views.py:453
msgid "rights"
msgstr "droits"
#: logs/views.py:476
#: logs/views.py:482
msgid "actions"
msgstr "actions"
#: logs/views.py:554
msgid "No model found."
msgstr "Aucun modèle trouvé."
#: logs/views.py:560
#: logs/views.py:532 logs/views.py:583
msgid "Nonexistent entry."
msgstr "Entrée inexistante."
#: logs/views.py:567
#: logs/views.py:545
msgid "You don't have the right to access this menu."
msgstr "Vous n'avez pas le droit d'accéder à ce menu."
#: logs/views.py:570 logs/views.py:626
msgid "No model found."
msgstr "Aucun modèle trouvé."

View file

@ -28,13 +28,15 @@ from django.contrib.auth.models import Group
from machines.models import IpList
from machines.models import Interface
from machines.models import Machine
from machines.models import MachineType
from users.models import User
from users.models import Adherent
from users.models import Club
from topologie.models import Room
from topologie.models import Port
class MachineHistoryEvent:
class MachineHistorySearchEvent:
def __init__(self, user, machine, interface, start=None, end=None):
"""
:param user: User, The user owning the maching at the time of the event
@ -76,16 +78,16 @@ class MachineHistoryEvent:
)
class MachineHistory:
class MachineHistorySearch:
def __init__(self):
self.events = []
self.__last_evt = None
self._last_evt = None
def get(self, search, params):
"""
:param search: ip or mac to lookup
:param params: dict built by the search view
:return: list or None, a list of MachineHistoryEvent
:return: list or None, a list of MachineHistorySearchEvent in reverse chronological order
"""
self.start = params.get("s", None)
self.end = params.get("e", None)
@ -93,33 +95,33 @@ class MachineHistory:
self.events = []
if search_type == "ip":
return self.__get_by_ip(search)
return self._get_by_ip(search)[::-1]
elif search_type == "mac":
return self.__get_by_mac(search)
return self._get_by_mac(search)[::-1]
return None
def __add_revision(self, user, machine, interface):
def _add_revision(self, user, machine, interface):
"""
Add a new revision to the chronological order
:param user: User, The user owning the maching at the time of the event
:param machine: Version, the machine version related to the interface
:param interface: Version, the interface targeted by this event
"""
evt = MachineHistoryEvent(user, machine, interface)
evt = MachineHistorySearchEvent(user, machine, interface)
evt.start_date = interface.revision.date_created
# Try not to recreate events if it's unnecessary
if evt.is_similar(self.__last_evt):
if evt.is_similar(self._last_evt):
return
# Mark the end of validity of the last element
if self.__last_evt and not self.__last_evt.end_date:
self.__last_evt.end_date = evt.start_date
if self._last_evt and not self._last_evt.end_date:
self._last_evt.end_date = evt.start_date
# If the event ends before the given date, remove it
if self.start and evt.start_date.date() < self.start:
self.__last_evt = None
self._last_evt = None
self.events.pop()
# Make sure the new event starts before the given end date
@ -128,9 +130,9 @@ class MachineHistory:
# Save the new element
self.events.append(evt)
self.__last_evt = evt
self._last_evt = evt
def __get_interfaces_for_ip(self, ip):
def _get_interfaces_for_ip(self, ip):
"""
:param ip: str
:return: An iterable object with the Version objects
@ -147,7 +149,7 @@ class MachineHistory:
Version.objects.get_for_model(Interface).order_by("revision__date_created")
)
def __get_interfaces_for_mac(self, mac):
def _get_interfaces_for_mac(self, mac):
"""
:param mac: str
:return: An iterable object with the Version objects
@ -158,7 +160,7 @@ class MachineHistory:
Version.objects.get_for_model(Interface).order_by("revision__date_created")
)
def __get_machines_for_interface(self, interface):
def _get_machines_for_interface(self, interface):
"""
:param interface: Version, the interface for which to find the machines
:return: An iterable object with the Version objects of Machine to
@ -170,7 +172,7 @@ class MachineHistory:
Version.objects.get_for_model(Machine).order_by("revision__date_created")
)
def __get_user_for_machine(self, machine):
def _get_user_for_machine(self, machine):
"""
:param machine: Version, the machine of which the owner must be found
:return: The user to which the given machine belongs
@ -179,48 +181,67 @@ class MachineHistory:
user_id = machine.field_dict["user_id"]
return User.objects.get(id=user_id)
def __get_by_ip(self, ip):
def _get_by_ip(self, ip):
"""
:param ip: str, The IP to lookup
:returns: list, a list of MachineHistoryEvent
:returns: list, a list of MachineHistorySearchEvent
"""
interfaces = self.__get_interfaces_for_ip(ip)
interfaces = self._get_interfaces_for_ip(ip)
for interface in interfaces:
machines = self.__get_machines_for_interface(interface)
machines = self._get_machines_for_interface(interface)
for machine in machines:
user = self.__get_user_for_machine(machine)
self.__add_revision(user, machine, interface)
user = self._get_user_for_machine(machine)
self._add_revision(user, machine, interface)
return self.events
def __get_by_mac(self, mac):
def _get_by_mac(self, mac):
"""
:param mac: str, The MAC address to lookup
:returns: list, a list of MachineHistoryEvent
:returns: list, a list of MachineHistorySearchEvent
"""
interfaces = self.__get_interfaces_for_mac(mac)
interfaces = self._get_interfaces_for_mac(mac)
for interface in interfaces:
machines = self.__get_machines_for_interface(interface)
machines = self._get_machines_for_interface(interface)
for machine in machines:
user = self.__get_user_for_machine(machine)
self.__add_revision(user, machine, interface)
user = self._get_user_for_machine(machine)
self._add_revision(user, machine, interface)
return self.events
class UserHistoryEvent:
def __init__(self, user, version, previous_version=None, edited_fields=None):
class RelatedHistory:
def __init__(self, name, model_name, object_id):
"""
: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 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
def __eq__(self, other):
return (
self.model_name == other.model_name
and self.object_id == other.object_id
)
def __hash__(self):
return hash((self.model_name, self.object_id))
class HistoryEvent:
def __init__(self, version, previous_version=None, edited_fields=None):
"""
:param version: Version, the version of the object for this event
:param previous_version: Version, the version of the object 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
@ -228,7 +249,109 @@ class UserHistoryEvent:
self.performed_by = version.revision.user
self.comment = version.revision.get_comment() or None
def __repr(self, name, value):
def _repr(self, name, value):
"""
Returns the best representation of the given field
:param name: the name of the field
:param value: the value of the field
:return: object
"""
if value is None:
return _("None")
return value
def edits(self, hide=[]):
"""
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._repr(field, self.previous_version.field_dict[field]),
self._repr(field, self.version.field_dict[field])
))
return edits
class History:
def __init__(self):
self.name = None
self.events = []
self.related = [] # For example, a machine has a list of its interfaces
self._last_version = None
self.event_type = HistoryEvent
def get(self, instance_id, model):
"""
:param instance_id: int, The id of the instance to lookup
:param model: class, The type of object to lookup
:return: list or None, a list of HistoryEvent, in reverse chronological order
"""
self.events = []
# Get all the versions for this instance, with the oldest first
self._last_version = None
interface_versions = filter(
lambda x: x.field_dict["id"] == instance_id,
Version.objects.get_for_model(model).order_by("revision__date_created")
)
for version in interface_versions:
self._add_revision(version)
# Return None if interface_versions was empty
if self._last_version is None:
return None
return self.events[::-1]
def _compute_diff(self, v1, v2, ignoring=[]):
"""
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, version):
"""
Add a new revision to the chronological order
:param version: Version, The version of the interface for this event
"""
diff = None
if self._last_version is not None:
diff = self._compute_diff(version, self._last_version)
# Ignore "empty" events
if not diff:
self._last_version = version
return
evt = self.event_type(version, self._last_version, diff)
self.events.append(evt)
self._last_version = version
class UserHistoryEvent(HistoryEvent):
def _repr(self, name, value):
"""
Returns the best representation of the given field
:param name: the name of the field
@ -285,10 +408,7 @@ class UserHistoryEvent:
return ", ".join(users)
if value is None:
return _("None")
return value
return super(UserHistoryEvent, self)._repr(name, value)
def edits(self, hide=["password", "pwd_ntlm", "gpg_fingerprint"]):
"""
@ -296,79 +416,94 @@ class UserHistoryEvent:
: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.__repr(field, self.previous_version.field_dict[field]),
self.__repr(field, self.version.field_dict[field])
))
return edits
return super(UserHistoryEvent, self).edits(hide)
def __eq__(self, other):
return (
self.user.id == other.user.id
and self.edited_fields == other.edited_fields
self.edited_fields == other.edited_fields
and self.date == other.date
and self.performed_by == other.performed_by
and self.comment == other.comment
)
def __hash__(self):
return hash((self.user.id, frozenset(self.edited_fields), self.date, self.performed_by, self.comment))
return hash((frozenset(self.edited_fields), self.date, self.performed_by, self.comment))
def __repr__(self):
return "{} edited fields {} of {} ({})".format(
return "{} edited fields {} ({})".format(
self.performed_by,
self.edited_fields or "nothing",
self.user,
self.comment or "No comment"
)
class UserHistory:
class UserHistory(History):
def __init__(self):
self.events = []
self.__last_version = None
super(UserHistory, self).__init__()
self.event_type = UserHistoryEvent
def get(self, user):
def get(self, user_id):
"""
:param user: User, 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
"""
self.events = []
# Find whether this is a Club or an Adherent
try:
obj = Adherent.objects.get(user_ptr_id=user.id)
except Adherent.DoesNotExist:
obj = Club.objects.get(user_ptr_id=user.id)
# Try to find an Adherent object
adherents = filter(
lambda x: x.field_dict["user_ptr_id"] == user_id,
Version.objects.get_for_model(Adherent)
)
obj = next(adherents, None)
model = Adherent
# Fallback on a Club
if obj is None:
clubs = filter(
lambda x: x.field_dict["user_ptr_id"] == user_id,
Version.objects.get_for_model(Club)
)
obj = next(clubs, None)
model = Club
# If nothing was found, abort
if obj is None:
return None
# Add in "related" elements the list of Machine objects
# that were once owned by this user
self.related = filter(
lambda x: x.field_dict["user_id"] == user_id,
Version.objects.get_for_model(Machine).order_by("-revision__date_created")
)
self.related = [RelatedHistory(
m.field_dict["name"] or _("None"),
"machine",
m.field_dict["id"]) for m in self.related]
self.related = list(dict.fromkeys(self.related))
# Get all the versions for this user, with the oldest first
self.__last_version = None
self._last_version = None
user_versions = filter(
lambda x: x.field_dict["id"] == user.id,
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(user, version)
self._add_revision(version)
# Update name
self.name = self._last_version.field_dict["pseudo"]
# Do the same thing for the Adherent of Club
self.__last_version = None
self._last_version = None
obj_versions = filter(
lambda x: x.field_dict["id"] == obj.id,
Version.objects.get_for_model(type(obj)).order_by("revision__date_created")
lambda x: x.field_dict["id"] == user_id,
Version.objects.get_for_model(model).order_by("revision__date_created")
)
for version in obj_versions:
self.__add_revision(user, version)
self._add_revision(version)
# Remove duplicates and sort
self.events = list(dict.fromkeys(self.events))
@ -378,37 +513,121 @@ class UserHistory:
reverse=True
)
def __compute_diff(self, v1, v2, ignoring=["last_login", "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):
def _add_revision(self, 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)
if self._last_version is not None:
diff = self._compute_diff(
version,
self._last_version,
ignoring=["last_login", "pwd_ntlm", "email_change_date"]
)
# Ignore "empty" events like login
if not diff:
self.__last_version = version
self._last_version = version
return
evt = UserHistoryEvent(user, version, self.__last_version, diff)
evt = UserHistoryEvent(version, self._last_version, diff)
self.events.append(evt)
self.__last_version = version
self._last_version = version
class MachineHistoryEvent(HistoryEvent):
def _repr(self, name, value):
"""
Returns the best representation of the given field
:param name: the name of the field
:param value: the value of the field
:return: object
"""
if name == "user_id":
try:
return User.objects.get(id=value).pseudo
except User.DoesNotExist:
return "{} ({})".format(_("Deleted"), value)
return super(MachineHistoryEvent, self)._repr(name, value)
class MachineHistory(History):
def __init__(self):
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(filter(
lambda x: x.field_dict["machine_id"] == machine_id,
Version.objects.get_for_model(Interface).order_by("-revision__date_created")
))
# 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 = 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
class InterfaceHistoryEvent(HistoryEvent):
def _repr(self, name, value):
"""
Returns the best representation of the given field
:param name: the name of the field
:param value: the value of the field
:return: object
"""
if name == "ipv4_id" and value is not None:
try:
return IpList.objects.get(id=value)
except IpList.DoesNotExist:
return "{} ({})".format(_("Deleted"), value)
elif name == "machine_type_id":
try:
return MachineType.objects.get(id=value).name
except MachineType.DoesNotExist:
return "{} ({})".format(_("Deleted"), value)
elif name == "machine_id":
try:
return Machine.objects.get(id=value).get_name() or _("No name")
except Machine.DoesNotExist:
return "{} ({})".format(_("Deleted"), value)
elif name == "port_lists":
if len(value) == 0:
return _("None")
ports = []
for pid in value:
try:
ports.append(Port.objects.get(id=pid).pretty_name())
except Group.DoesNotExist:
ports.append("{} ({})".format(_("Deleted"), pid))
return super(InterfaceHistoryEvent, self)._repr(name, value)
class InterfaceHistory(History):
def __init__(self):
super(InterfaceHistory, self).__init__()
self.event_type = InterfaceHistoryEvent
def get(self, interface_id):
events = super(InterfaceHistory, self).get(interface_id, Interface)
# Update name
self.name = self._last_version.field_dict["mac_address"]
return events

View file

@ -23,11 +23,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %}
{% load i18n %}
{% load logs_extra %}
{% block title %}{% trans "History" %}{% endblock %}
{% block content %}
<h2>{% blocktrans %}History of {{ user }}{% endblocktrans %}</h2>
<h2>{% blocktrans %}History of {{ title }}{% endblocktrans %}</h2>
{% if events %}
<table class="table table-striped">
@ -73,6 +74,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% 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 />

View file

@ -42,7 +42,7 @@ def is_facture(baseinvoice):
@register.inclusion_tag("buttons/history.html")
def history_button(instance, text=False, html_class=True):
def history_button(instance, text=False, detailed=False, html_class=True):
"""Creates the correct history button for an instance.
Args:
@ -57,5 +57,6 @@ def history_button(instance, text=False, html_class=True):
"name": instance._meta.model_name,
"id": instance.id,
"text": text,
"detailed": detailed,
"class": html_class,
}

View file

@ -37,15 +37,19 @@ urlpatterns = [
views.revert_action,
name="revert-action",
),
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"),
url(r"^stats_actions/$", views.stats_actions, name="stats-actions"),
url(
r"(?P<application>\w+)/(?P<object_name>\w+)/(?P<object_id>[0-9]+)$",
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"),
url(r"^stats_actions/$", views.stats_actions, name="stats-actions"),
url(r"^stats_search_machine/$", views.stats_search_machine_history, name="stats-search-machine"),
url(r"^user/(?P<userid>[0-9]+)$", views.user_history, name="user-history"),
]

View file

@ -101,8 +101,14 @@ from re2o.utils import (
from re2o.base import re2o_paginator, SortTable
from re2o.acl import can_view_all, can_view_app, can_edit_history, can_view
from .models import MachineHistory, UserHistory
from .forms import MachineHistoryForm
from .models import (
MachineHistorySearch,
UserHistory,
MachineHistory,
InterfaceHistory
)
from .forms import MachineHistorySearchForm
@login_required
@ -486,9 +492,9 @@ def stats_actions(request):
def stats_search_machine_history(request):
"""View which displays the history of machines with the given
une IP or MAC adresse"""
history_form = MachineHistoryForm(request.GET or None)
history_form = MachineHistorySearchForm(request.GET or None)
if history_form.is_valid():
history = MachineHistory()
history = MachineHistorySearch()
events = history.get(
history_form.cleaned_data.get("q", ""),
history_form.cleaned_data
@ -508,32 +514,98 @@ def stats_search_machine_history(request):
return render(request, "logs/search_machine_history.html", {"history_form": history_form})
@login_required
@can_view(User)
def user_history(request, users, **_kwargs):
history = UserHistory()
events = history.get(users)
def get_history_object(request, model, object_name, object_id, allow_deleted=False):
"""Get the objet of type model with the given object_id
Handles permissions and DoesNotExist errors
"""
is_deleted = False
max_result = GeneralOption.get_cached_value("pagination_number")
events = re2o_paginator(
request,
events,
max_result
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")
msg = None
else:
can_view, msg, _permissions = instance.can_view(request.user)
if not can_view:
messages.error(
request, msg or _("You don't have the right to access this menu.")
)
return False, redirect(
reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
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/user_history.html",
{ "user": users, "events": events },
"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.
Args:
request: The request sent by the user.
@ -552,23 +624,11 @@ def history(request, application, object_name, object_id):
model = apps.get_model(application, object_name)
except LookupError:
raise Http404(_("No model found."))
object_name_id = object_name + "id"
kwargs = {object_name_id: object_id}
try:
instance = model.get_instance(**kwargs)
except model.DoesNotExist:
messages.error(request, _("Nonexistent entry."))
return redirect(
reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
can, msg, _permissions = instance.can_view(request.user)
if not can:
messages.error(
request, msg or _("You don't have the right to access this menu.")
)
return redirect(
reverse("users:profil", kwargs={"userid": str(request.user.id)})
)
can_view, 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"):

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-06-23 16:35+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

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 %}
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %}
{% acl_end %}
{% history_button machine %}
{% history_button machine detailed=True %}
{% 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 %}
{% history_button interface detailed=True %}
{% can_delete interface %}
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
{% acl_end %}

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2019-11-16 00:22+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-06-24 15:54+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -342,13 +342,13 @@ msgid "Maximum number of local email addresses for a standard user."
msgstr ""
"Nombre maximum d'adresses mail locales autorisé pour un utilisateur standard."
#: preferences/models.py:122
#: preferences/models.py:125
msgid "Not yet active users will be deleted after this number of days."
msgstr ""
"Les utilisateurs n'ayant jamais adhéré seront supprimés après ce nombre de "
"jours."
#: preferences/models.py:127
#: preferences/models.py:130
msgid ""
"Users with an email address not yet confirmed will be disabled after this "
"number of days."
@ -356,11 +356,11 @@ msgstr ""
"Les utilisateurs n'ayant pas confirmé leur addresse mail seront désactivés "
"après ce nombre de jours"
#: preferences/models.py:131
#: preferences/models.py:134
msgid "A new user can create their account on Re2o."
msgstr "Un nouvel utilisateur peut créer son compte sur Re2o."
#: preferences/models.py:136
#: preferences/models.py:139
msgid ""
"If True, all new created and connected users are active. If False, only when "
"a valid registration has been paid."
@ -368,7 +368,7 @@ msgstr ""
"Si True, tous les nouveaux utilisations créés et connectés sont actifs. Si "
"False, seulement quand une inscription validée a été payée."
#: preferences/models.py:143
#: preferences/models.py:146
msgid ""
"If True, users have the choice to receive an email containing a link to "
"reset their password during creation, or to directly set their password in "
@ -379,172 +379,172 @@ msgstr ""
"de choisir leur mot de passe immédiatement. Si False, un mail est toujours "
"envoyé."
#: preferences/models.py:150
#: preferences/models.py:153
msgid "If True, archived users are allowed to connect."
msgstr "Si True, les utilisateurs archivés sont autorisés à se connecter."
#: preferences/models.py:154
#: preferences/models.py:157
msgid "Can view the user preferences"
msgstr "Peut voir les préférences d'utilisateur"
#: preferences/models.py:155
#: preferences/models.py:158
msgid "user preferences"
msgstr "Préférences d'utilisateur"
#: preferences/models.py:162
#: preferences/models.py:165
msgid "Email domain must begin with @."
msgstr "Un domaine mail doit commencer par @."
#: preferences/models.py:180
#: preferences/models.py:183
msgid "Automatic configuration by RA"
msgstr "Configuration automatique par RA"
#: preferences/models.py:181
#: preferences/models.py:184
msgid "IP addresses assignment by DHCPv6"
msgstr "Attribution d'adresses IP par DHCPv6"
#: preferences/models.py:182
#: preferences/models.py:185
msgid "Disabled"
msgstr "Désactivé"
#: preferences/models.py:191
#: preferences/models.py:194
msgid "default Time To Live (TTL) for CNAME, A and AAAA records"
msgstr ""
"Temps de vie (TTL) par défault pour des enregistrements CNAME, A et AAAA"
#: preferences/models.py:201
#: preferences/models.py:204
msgid "Can view the machine preferences"
msgstr "Peut voir les préférences de machine"
#: preferences/models.py:202
#: preferences/models.py:205
msgid "machine preferences"
msgstr "Préférences de machine"
#: preferences/models.py:222 preferences/models.py:684
#: preferences/models.py:225 preferences/models.py:687
msgid "On the IP range's VLAN of the machine"
msgstr "Sur le VLAN de la plage d'IP de la machine"
#: preferences/models.py:223 preferences/models.py:685
#: preferences/models.py:226 preferences/models.py:688
msgid "Preset in \"VLAN for machines accepted by RADIUS\""
msgstr "Prédéfinie dans « VLAN pour les machines acceptées par RADIUS »"
#: preferences/models.py:229
#: preferences/models.py:232
msgid "Web management, activated in case of automatic provision."
msgstr "Gestion web, activée en cas de provision automatique."
#: preferences/models.py:234
#: preferences/models.py:237
msgid ""
"SSL web management, make sure that a certificate is installed on the switch."
msgstr ""
"Gestion web SSL, vérifiez qu'un certificat est installé sur le commutateur "
"réseau."
#: preferences/models.py:240
#: preferences/models.py:243
msgid "REST management, activated in case of automatic provision."
msgstr "Gestion REST, activée en cas de provision automatique."
#: preferences/models.py:247
#: preferences/models.py:250
msgid "IP range for the management of switches."
msgstr "Plage d'IP pour la gestion des commutateurs réseau."
#: preferences/models.py:253
#: preferences/models.py:256
msgid "Provision of configuration mode for switches."
msgstr "Mode de provision de configuration pour les commutateurs réseau."
#: preferences/models.py:256
#: preferences/models.py:259
msgid "SFTP login for switches."
msgstr "Identifiant SFTP pour les commutateurs réseau."
#: preferences/models.py:259
#: preferences/models.py:262
msgid "SFTP password."
msgstr "Mot de passe SFTP."
#: preferences/models.py:364
#: preferences/models.py:367
msgid "Can view the topology preferences"
msgstr "Peut voir les préférences de topologie"
#: preferences/models.py:366
#: preferences/models.py:369
msgid "topology preferences"
msgstr "préférences de topologie"
#: preferences/models.py:379
#: preferences/models.py:382
msgid "RADIUS key."
msgstr "Clé RADIUS."
#: preferences/models.py:381
#: preferences/models.py:384
msgid "Comment for this key."
msgstr "Commentaire pour cette clé."
#: preferences/models.py:384
#: preferences/models.py:387
msgid "Default key for switches."
msgstr "Clé par défaut pour les commutateurs réseau."
#: preferences/models.py:388
#: preferences/models.py:391
msgid "Can view a RADIUS key object"
msgstr "Peut voir un objet clé RADIUS"
#: preferences/models.py:389 preferences/views.py:335
#: preferences/models.py:392 preferences/views.py:335
msgid "RADIUS key"
msgstr "Clé RADIUS"
#: preferences/models.py:390
#: preferences/templates/preferences/display_preferences.html:221
#: preferences/models.py:393
#: preferences/templates/preferences/display_preferences.html:223
msgid "RADIUS keys"
msgstr "clés RADIUS"
#: preferences/models.py:397
#: preferences/models.py:400
msgid "Default RADIUS key for switches already exists."
msgstr "Clé par défaut pour les commutateurs réseau."
#: preferences/models.py:400
#: preferences/models.py:403
msgid "RADIUS key "
msgstr "clé RADIUS "
#: preferences/models.py:406
#: preferences/models.py:409
msgid "Switch login."
msgstr "Identifiant du commutateur réseau."
#: preferences/models.py:407
#: preferences/models.py:410
msgid "Password."
msgstr "Mot de passe."
#: preferences/models.py:409
#: preferences/models.py:412
msgid "Default credentials for switches."
msgstr "Identifiants par défaut pour les commutateurs réseau."
#: preferences/models.py:416
#: preferences/models.py:419
msgid "Can view a switch management credentials object"
msgstr "Peut voir un objet identifiants de gestion de commutateur réseau"
#: preferences/models.py:419 preferences/views.py:397
#: preferences/models.py:422 preferences/views.py:397
msgid "switch management credentials"
msgstr "identifiants de gestion de commutateur réseau"
#: preferences/models.py:422
#: preferences/models.py:425
msgid "Switch login "
msgstr "Identifiant du commutateur réseau "
#: preferences/models.py:434
#: preferences/models.py:437
msgid "Delay between the email and the membership's end."
msgstr "Délai entre le mail et la fin d'adhésion."
#: preferences/models.py:440
#: preferences/models.py:443
msgid "Message displayed specifically for this reminder."
msgstr "Message affiché spécifiquement pour ce rappel."
#: preferences/models.py:444
#: preferences/models.py:447
msgid "Can view a reminder object"
msgstr "Peut voir un objet rappel"
#: preferences/models.py:445 preferences/views.py:280
#: preferences/models.py:448 preferences/views.py:280
msgid "reminder"
msgstr "rappel"
#: preferences/models.py:446
#: preferences/models.py:449
msgid "reminders"
msgstr "rappels"
#: preferences/models.py:467
#: preferences/models.py:470
msgid ""
"General message displayed on the French version of the website (e.g. in case "
"of maintenance)."
@ -552,7 +552,7 @@ msgstr ""
"Message général affiché sur la version française du site (ex : en cas de "
"maintenance)."
#: preferences/models.py:475
#: preferences/models.py:478
msgid ""
"General message displayed on the English version of the website (e.g. in "
"case of maintenance)."
@ -560,75 +560,75 @@ msgstr ""
"Message général affiché sur la version anglaise du site (ex : en cas de "
"maintenance)."
#: preferences/models.py:490
#: preferences/models.py:493
msgid "Can view the general preferences"
msgstr "Peut voir les préférences générales"
#: preferences/models.py:491
#: preferences/models.py:494
msgid "general preferences"
msgstr "préférences générales"
#: preferences/models.py:511
#: preferences/models.py:514
msgid "Can view the service preferences"
msgstr "Peut voir les préférences de service"
#: preferences/models.py:512 preferences/views.py:231
#: preferences/models.py:515 preferences/views.py:231
msgid "service"
msgstr "service"
#: preferences/models.py:513
#: preferences/models.py:516
msgid "services"
msgstr "services"
#: preferences/models.py:523
#: preferences/models.py:526
msgid "Contact email address."
msgstr "Adresse mail de contact."
#: preferences/models.py:529
#: preferences/models.py:532
msgid "Description of the associated email address."
msgstr "Description de l'adresse mail associée."
#: preferences/models.py:539
#: preferences/models.py:542
msgid "Can view a contact email address object"
msgstr "Peut voir un objet adresse mail de contact"
#: preferences/models.py:541
#: preferences/models.py:544
msgid "contact email address"
msgstr "adresse mail de contact"
#: preferences/models.py:542
#: preferences/models.py:545
msgid "contact email addresses"
msgstr "adresses mail de contact"
#: preferences/models.py:550 preferences/views.py:635
#: preferences/models.py:553 preferences/views.py:635
msgid "mandate"
msgstr "mandat"
#: preferences/models.py:551
#: preferences/models.py:554
msgid "mandates"
msgstr "mandats"
#: preferences/models.py:552
#: preferences/models.py:555
msgid "Can view a mandate object"
msgstr "Peut voir un objet mandat"
#: preferences/models.py:559
#: preferences/models.py:562
msgid "president of the association"
msgstr "président de l'association"
#: preferences/models.py:560
#: preferences/models.py:563
msgid "Displayed on subscription vouchers."
msgstr "Affiché sur les reçus de cotisation."
#: preferences/models.py:562
#: preferences/models.py:565
msgid "start date"
msgstr "date de début"
#: preferences/models.py:563
#: preferences/models.py:566
msgid "end date"
msgstr "date de fin"
#: preferences/models.py:577
#: preferences/models.py:580
msgid ""
"No mandates have been created. Please go to the preferences page to create "
"one."
@ -636,140 +636,140 @@ msgstr ""
"Aucun mandat n'a été créé. Veuillez vous rendre sur la page de préférences "
"pour en créer un."
#: preferences/models.py:593
#: preferences/models.py:596
msgid "Networking organisation school Something"
msgstr "Association de réseau de l'école Machin"
#: preferences/models.py:596
#: preferences/models.py:599
msgid "Threadneedle Street"
msgstr "1 rue de la Vrillière"
#: preferences/models.py:597
#: preferences/models.py:600
msgid "London EC2R 8AH"
msgstr "75001 Paris"
#: preferences/models.py:600
#: preferences/models.py:603
msgid "Organisation"
msgstr "Association"
#: preferences/models.py:607
#: preferences/models.py:610
msgid "Can view the organisation preferences"
msgstr "Peut voir les préférences d'association"
#: preferences/models.py:608
#: preferences/models.py:611
msgid "organisation preferences"
msgstr "préférences d'association"
#: preferences/models.py:626
#: preferences/models.py:629
msgid "Can view the homepage preferences"
msgstr "Peut voir les préférences de page d'accueil"
#: preferences/models.py:627
#: preferences/models.py:630
msgid "homepage preferences"
msgstr "Préférences de page d'accueil"
#: preferences/models.py:641
#: preferences/models.py:644
msgid "Welcome email in French."
msgstr "Mail de bienvenue en français."
#: preferences/models.py:644
#: preferences/models.py:647
msgid "Welcome email in English."
msgstr "Mail de bienvenue en anglais."
#: preferences/models.py:649
#: preferences/models.py:652
msgid "Can view the email message preferences"
msgstr "Peut voir les préférences de message pour les mails"
#: preferences/models.py:651
#: preferences/models.py:654
msgid "email message preferences"
msgstr "préférences de messages pour les mails"
#: preferences/models.py:656
#: preferences/models.py:659
msgid "RADIUS attribute"
msgstr "attribut RADIUS"
#: preferences/models.py:657
#: preferences/models.py:660
msgid "RADIUS attributes"
msgstr "attributs RADIUS"
#: preferences/models.py:661 preferences/views.py:588
#: preferences/models.py:664 preferences/views.py:588
msgid "attribute"
msgstr "attribut"
#: preferences/models.py:662
#: preferences/models.py:665
msgid "See https://freeradius.org/rfc/attributes.html."
msgstr "Voir https://freeradius.org/rfc/attributes.html."
#: preferences/models.py:664
#: preferences/models.py:667
msgid "value"
msgstr "valeur"
#: preferences/models.py:666
#: preferences/models.py:669
msgid "comment"
msgstr "commentaire"
#: preferences/models.py:667
#: preferences/models.py:670
msgid "Use this field to document this attribute."
msgstr "Utilisez ce champ pour documenter cet attribut."
#: preferences/models.py:678
#: preferences/models.py:681
msgid "RADIUS policy"
msgstr "politique de RADIUS"
#: preferences/models.py:679
#: preferences/templates/preferences/display_preferences.html:299
#: preferences/models.py:682
#: preferences/templates/preferences/display_preferences.html:301
msgid "RADIUS policies"
msgstr "politiques de RADIUS"
#: preferences/models.py:690
#: preferences/models.py:693
msgid "Reject the machine"
msgstr "Rejeter la machine"
#: preferences/models.py:691
#: preferences/models.py:694
msgid "Place the machine on the VLAN"
msgstr "Placer la machine sur le VLAN"
#: preferences/models.py:700
#: preferences/models.py:703
msgid "policy for unknown machines"
msgstr "politique pour les machines inconnues"
#: preferences/models.py:708
#: preferences/models.py:711
msgid "unknown machines VLAN"
msgstr "VLAN pour les machines inconnues"
#: preferences/models.py:709
#: preferences/models.py:712
msgid "VLAN for unknown machines if not rejected."
msgstr "VLAN pour les machines inconnues si non rejeté."
#: preferences/models.py:715
#: preferences/models.py:718
msgid "unknown machines attributes"
msgstr "attributs pour les machines inconnues"
#: preferences/models.py:716
#: preferences/models.py:719
msgid "Answer attributes for unknown machines."
msgstr "Attributs de réponse pour les machines inconnues."
#: preferences/models.py:722
#: preferences/models.py:725
msgid "policy for unknown ports"
msgstr "politique pour les ports inconnus"
#: preferences/models.py:730
#: preferences/models.py:733
msgid "unknown ports VLAN"
msgstr "VLAN pour les ports inconnus"
#: preferences/models.py:731
#: preferences/models.py:734
msgid "VLAN for unknown ports if not rejected."
msgstr "VLAN pour les ports inconnus si non rejeté."
#: preferences/models.py:737
#: preferences/models.py:740
msgid "unknown ports attributes"
msgstr "attributs pour les ports inconnus"
#: preferences/models.py:738
#: preferences/models.py:741
msgid "Answer attributes for unknown ports."
msgstr "Attributs de réponse pour les ports inconnus."
#: preferences/models.py:745
#: preferences/models.py:748
msgid ""
"Policy for machines connecting from unregistered rooms (relevant on ports "
"with STRICT RADIUS mode)"
@ -777,87 +777,87 @@ msgstr ""
"Politique pour les machines se connectant depuis des chambre non "
"enregistrées (pertinent pour les ports avec le mode de RADIUS STRICT)"
#: preferences/models.py:755
#: preferences/models.py:758
msgid "unknown rooms VLAN"
msgstr "VLAN pour les chambres inconnues"
#: preferences/models.py:756
#: preferences/models.py:759
msgid "VLAN for unknown rooms if not rejected."
msgstr "VLAN pour les chambres inconnues si non rejeté."
#: preferences/models.py:762
#: preferences/models.py:765
msgid "unknown rooms attributes"
msgstr "attributs pour les chambres inconnues"
#: preferences/models.py:763
#: preferences/models.py:766
msgid "Answer attributes for unknown rooms."
msgstr "Attributs de réponse pour les chambres inconnues."
#: preferences/models.py:769
#: preferences/models.py:772
msgid "policy for non members"
msgstr "politique pour les non adhérents"
#: preferences/models.py:777
#: preferences/models.py:780
msgid "non members VLAN"
msgstr "VLAN pour les non adhérents"
#: preferences/models.py:778
#: preferences/models.py:781
msgid "VLAN for non members if not rejected."
msgstr "VLAN pour les non adhérents si non rejeté."
#: preferences/models.py:784
#: preferences/models.py:787
msgid "non members attributes"
msgstr "attributs pour les non adhérents"
#: preferences/models.py:785
#: preferences/models.py:788
msgid "Answer attributes for non members."
msgstr "Attributs de réponse pour les non adhérents."
#: preferences/models.py:791
#: preferences/models.py:794
msgid "policy for banned users"
msgstr "politique pour les utilisateurs bannis"
#: preferences/models.py:799
#: preferences/models.py:802
msgid "banned users VLAN"
msgstr "VLAN pour les utilisateurs bannis"
#: preferences/models.py:800
#: preferences/models.py:803
msgid "VLAN for banned users if not rejected."
msgstr "VLAN pour les utilisateurs bannis si non rejeté."
#: preferences/models.py:806
#: preferences/models.py:809
msgid "banned users attributes"
msgstr "attributs pour les utilisateurs bannis"
#: preferences/models.py:807
#: preferences/models.py:810
msgid "Answer attributes for banned users."
msgstr "Attributs de réponse pour les utilisateurs bannis."
#: preferences/models.py:820
#: preferences/models.py:823
msgid "accepted users attributes"
msgstr "attributs pour les utilisateurs acceptés"
#: preferences/models.py:821
#: preferences/models.py:824
msgid "Answer attributes for accepted users."
msgstr "Attributs de réponse pour les utilisateurs acceptés."
#: preferences/models.py:848
#: preferences/models.py:851
msgid "subscription preferences"
msgstr "préférences de cotisation"
#: preferences/models.py:852
#: preferences/models.py:855
msgid "template for invoices"
msgstr "modèle pour les factures"
#: preferences/models.py:859
#: preferences/models.py:862
msgid "template for subscription vouchers"
msgstr "modèle pour les reçus de cotisation"
#: preferences/models.py:865
#: preferences/models.py:868
msgid "send voucher by email when the invoice is controlled"
msgstr "envoyer le reçu par mail quand la facture est contrôlée"
#: preferences/models.py:867
#: preferences/models.py:870
msgid ""
"Be careful, if no mandate is defined on the preferences page, errors will be "
"triggered when generating vouchers."
@ -865,19 +865,19 @@ msgstr ""
"Faites attention, si aucun mandat n'est défini sur la page de préférences, "
"des erreurs seront déclenchées en générant des reçus."
#: preferences/models.py:879
#: preferences/models.py:882
msgid "template"
msgstr "modèle"
#: preferences/models.py:880
#: preferences/models.py:883
msgid "name"
msgstr "nom"
#: preferences/models.py:883
#: preferences/models.py:886
msgid "document template"
msgstr "modèle de document"
#: preferences/models.py:884
#: preferences/models.py:887
msgid "document templates"
msgstr "modèles de document"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-06-24 20:10+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -24,8 +24,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load i18n %}
{% if name == "user" %}
<a {% if class%}class="btn btn-info btn-sm"{% endif %} role="button" title="{% trans "History" %}" href="{% url 'logs:user-history' id %}">
{% 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 %}

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -240,7 +240,8 @@ msgstr ""
msgid "Edit"
msgstr "Modifier"
#: templates/buttons/history.html:26 templates/buttons/history.html:27
#: templates/buttons/history.html:28 templates/buttons/history.html:29
#: templates/buttons/history.html:32 templates/buttons/history.html:33
msgid "History"
msgstr ""

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2019-11-16 00:35+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-06-25 14:53+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-23 14:44+0200\n"
"POT-Creation-Date: 2020-04-23 21:25+0200\n"
"PO-Revision-Date: 2018-06-27 23:35+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -56,7 +56,7 @@ msgid "The current password is incorrect."
msgstr "Le mot de passe actuel est incorrect."
#: users/forms.py:133 users/forms.py:181 users/forms.py:405
#: users/models.py:1912
#: users/models.py:1936
msgid "Password"
msgstr "Mot de passe"
@ -109,7 +109,7 @@ msgstr "Prénom"
msgid "Surname"
msgstr "Nom"
#: users/forms.py:321 users/forms.py:536 users/models.py:1912
#: users/forms.py:321 users/forms.py:536 users/models.py:1936
#: users/templates/users/aff_emailaddress.html:36
#: users/templates/users/profil.html:209
msgid "Email address"
@ -323,7 +323,7 @@ msgstr "Non confirmé"
msgid "Waiting for email confirmation"
msgstr "En attente de confirmation"
#: users/models.py:204 users/models.py:1565
#: users/models.py:204 users/models.py:1589
msgid "Must only contain letters, numerals or dashes."
msgstr "Doit seulement contenir des lettres, chiffres ou tirets."
@ -365,66 +365,72 @@ msgstr "Peut forcer le déménagement"
msgid "Can edit the shell of a user"
msgstr "Peut modifier l'interface en ligne de commande d'un utilisateur"
#: users/models.py:257
#: users/models.py:255
#, fuzzy
#| msgid "Can edit the state of a user"
msgid "Can edit the pseudo of a user"
msgstr "Peut changer l'état d'un utilisateur"
#: users/models.py:258
msgid "Can edit the groups of rights of a user (critical permission)"
msgstr ""
"Peut modifier les groupes de droits d'un utilisateur (permission critique)"
#: users/models.py:259
#: users/models.py:260
msgid "Can edit all users, including those with rights"
msgstr ""
"Peut modifier tous les utilisateurs, y compris ceux possédant des droits"
#: users/models.py:260
#: users/models.py:261
msgid "Can view a user object"
msgstr "Peut voir un objet utilisateur"
#: users/models.py:262
#: users/models.py:263
msgid "user (member or club)"
msgstr "utilisateur (adhérent ou club)"
#: users/models.py:263
#: users/models.py:264
msgid "users (members or clubs)"
msgstr "utilisateurs (adhérents ou clubs)"
#: users/models.py:281 users/models.py:309 users/models.py:319
#: users/models.py:282 users/models.py:310 users/models.py:320
msgid "Unknown type."
msgstr "Type inconnu."
#: users/models.py:315 users/templates/users/aff_listright.html:75
#: users/models.py:316 users/templates/users/aff_listright.html:75
#: users/templates/users/aff_listright.html:180
msgid "Member"
msgstr "Adhérent"
#: users/models.py:317
#: users/models.py:318
msgid "Club"
msgstr "Club"
#: users/models.py:896
#: users/models.py:897
msgid "Maximum number of registered machines reached."
msgstr "Nombre maximum de machines enregistrées atteint."
#: users/models.py:898
#: users/models.py:899
msgid "Re2o doesn't know wich machine type to assign."
msgstr "Re2o ne sait pas quel type de machine attribuer."
#: users/models.py:921 users/templates/users/user_autocapture.html:64
#: users/models.py:922 users/templates/users/user_autocapture.html:64
msgid "OK"
msgstr "OK"
#: users/models.py:1019
#: users/models.py:1020
msgid "This user is archived."
msgstr "Cet utilisateur est archivé."
#: users/models.py:1033 users/models.py:1087
#: users/models.py:1034 users/models.py:1088
msgid "You don't have the right to edit this club."
msgstr "Vous n'avez pas le droit de modifier ce club."
#: users/models.py:1045
#: users/models.py:1046
msgid "User with critical rights, can't be edited."
msgstr "Utilisateur avec des droits critiques, ne peut être modifié."
#: users/models.py:1052
#: users/models.py:1053
msgid ""
"Impossible to edit the organisation's user without the \"change_all_users\" "
"right."
@ -432,257 +438,263 @@ msgstr ""
"Impossible de modifier l'utilisateur de l'association sans le droit « "
"change_all_users »."
#: users/models.py:1064 users/models.py:1102
#: users/models.py:1065 users/models.py:1103
msgid "You don't have the right to edit another user."
msgstr "Vous n'avez pas le droit de modifier un autre utilisateur."
#: users/models.py:1128
#: users/models.py:1129
msgid "You don't have the right to change the room."
msgstr "Vous n'avez pas le droit de changer la chambre."
#: users/models.py:1145
#: users/models.py:1146
msgid "You don't have the right to change the state."
msgstr "Vous n'avez pas le droit de changer l'état."
#: users/models.py:1165
#: users/models.py:1166
msgid "You don't have the right to change the shell."
msgstr "Vous n'avez pas le droit de changer l'interface en ligne de commande."
#: users/models.py:1182 users/models.py:1197
#: users/models.py:1188
#, fuzzy
#| msgid "You don't have the right to change the state."
msgid "You don't have the right to change the pseudo."
msgstr "Vous n'avez pas le droit de changer l'état."
#: users/models.py:1205 users/models.py:1220
msgid "Local email accounts must be enabled."
msgstr "Les comptes mail locaux doivent être activés."
#: users/models.py:1212
#: users/models.py:1235
msgid "You don't have the right to force the move."
msgstr "Vous n'avez pas le droit de forcer le déménagement."
#: users/models.py:1227
#: users/models.py:1250
msgid "You don't have the right to edit the user's groups of rights."
msgstr ""
"Vous n'avez pas le droit de modifier les groupes de droits d'un autre "
"utilisateur."
#: users/models.py:1243
#: users/models.py:1266
msgid "\"superuser\" right required to edit the superuser flag."
msgstr "Droit « superuser » requis pour modifier le signalement superuser."
#: users/models.py:1268
#: users/models.py:1291
msgid "You don't have the right to view this club."
msgstr "Vous n'avez pas le droit de voir ce club."
#: users/models.py:1277
#: users/models.py:1300
msgid "You don't have the right to view another user."
msgstr "Vous n'avez pas le droit de voir un autre utilisateur."
#: users/models.py:1292 users/models.py:1501
#: users/models.py:1315 users/models.py:1525
msgid "You don't have the right to view the list of users."
msgstr "Vous n'avez pas le droit de voir la liste des utilisateurs."
#: users/models.py:1309
#: users/models.py:1332
msgid "You don't have the right to delete this user."
msgstr "Vous n'avez pas le droit de supprimer cet utilisateur."
#: users/models.py:1330
#: users/models.py:1354
msgid "This username is already used."
msgstr "Ce pseudo est déjà utilisé."
#: users/models.py:1337
#: users/models.py:1361
msgid "Email field cannot be empty."
msgstr "Le champ mail ne peut pas ^êêtre vide"
#: users/models.py:1344
#: users/models.py:1368
msgid "You can't use a {} address as an external contact address."
msgstr "Vous ne pouvez pas utiliser une adresse {} pour votre adresse externe."
#: users/models.py:1371
#: users/models.py:1395
msgid "member"
msgstr "adhérent"
#: users/models.py:1372
#: users/models.py:1396
msgid "members"
msgstr "adhérents"
#: users/models.py:1389
#: users/models.py:1413
msgid "A GPG fingerprint must contain 40 hexadecimal characters."
msgstr "Une empreinte GPG doit contenir 40 caractères hexadécimaux."
#: users/models.py:1414
#: users/models.py:1438
msgid "Self registration is disabled."
msgstr "L'auto inscription est désactivée."
#: users/models.py:1424
#: users/models.py:1448
msgid "You don't have the right to create a user."
msgstr "Vous n'avez pas le droit de créer un utilisateur."
#: users/models.py:1454
#: users/models.py:1478
msgid "club"
msgstr "club"
#: users/models.py:1455
#: users/models.py:1479
msgid "clubs"
msgstr "clubs"
#: users/models.py:1466
#: users/models.py:1490
msgid "You must be authenticated."
msgstr "Vous devez être authentifié."
#: users/models.py:1474
#: users/models.py:1498
msgid "You don't have the right to create a club."
msgstr "Vous n'avez pas le droit de créer un club."
#: users/models.py:1569
#: users/models.py:1593
msgid "Comment."
msgstr "Commentaire."
#: users/models.py:1575
#: users/models.py:1599
msgid "Can view a service user object"
msgstr "Peut voir un objet utilisateur service"
#: users/models.py:1576 users/views.py:349
#: users/models.py:1600 users/views.py:349
msgid "service user"
msgstr "utilisateur service"
#: users/models.py:1577
#: users/models.py:1601
msgid "service users"
msgstr "utilisateurs service"
#: users/models.py:1581
#: users/models.py:1605
#, python-brace-format
msgid "Service user <{name}>"
msgstr "Utilisateur service <{name}>"
#: users/models.py:1648
#: users/models.py:1672
msgid "Can view a school object"
msgstr "Peut voir un objet établissement"
#: users/models.py:1649
#: users/models.py:1673
msgid "school"
msgstr "établissement"
#: users/models.py:1650
#: users/models.py:1674
msgid "schools"
msgstr "établissements"
#: users/models.py:1669
#: users/models.py:1693
msgid "UNIX group names can only contain lower case letters."
msgstr ""
"Les noms de groupe UNIX peuvent seulement contenir des lettres minuscules."
#: users/models.py:1675
#: users/models.py:1699
msgid "Description."
msgstr "Description."
#: users/models.py:1678
#: users/models.py:1702
msgid "Can view a group of rights object"
msgstr "Peut voir un objet groupe de droits"
#: users/models.py:1679
#: users/models.py:1703
msgid "group of rights"
msgstr "groupe de droits"
#: users/models.py:1680
#: users/models.py:1704
msgid "groups of rights"
msgstr "groupes de droits"
#: users/models.py:1725
#: users/models.py:1749
msgid "Can view a shell object"
msgstr "Peut voir un objet interface en ligne de commande"
#: users/models.py:1726 users/views.py:649
#: users/models.py:1750 users/views.py:649
msgid "shell"
msgstr "interface en ligne de commande"
#: users/models.py:1727
#: users/models.py:1751
msgid "shells"
msgstr "interfaces en ligne de commande"
#: users/models.py:1745
#: users/models.py:1769
msgid "HARD (no access)"
msgstr "HARD (pas d'accès)"
#: users/models.py:1746
#: users/models.py:1770
msgid "SOFT (local access only)"
msgstr "SOFT (accès local uniquement)"
#: users/models.py:1747
#: users/models.py:1771
msgid "RESTRICTED (speed limitation)"
msgstr "RESTRICTED (limitation de vitesse)"
#: users/models.py:1758
#: users/models.py:1782
msgid "Can view a ban object"
msgstr "Peut voir un objet bannissement"
#: users/models.py:1759 users/views.py:400
#: users/models.py:1783 users/views.py:400
msgid "ban"
msgstr "bannissement"
#: users/models.py:1760
#: users/models.py:1784
msgid "bans"
msgstr "bannissements"
#: users/models.py:1797
#: users/models.py:1821
msgid "You don't have the right to view other bans than yours."
msgstr ""
"Vous n'avez pas le droit de voir d'autres bannissements que les vôtres."
#: users/models.py:1845
#: users/models.py:1869
msgid "Can view a whitelist object"
msgstr "Peut voir un objet accès gracieux"
#: users/models.py:1846
#: users/models.py:1870
msgid "whitelist (free of charge access)"
msgstr "Accès gracieux"
#: users/models.py:1847
#: users/models.py:1871
msgid "whitelists (free of charge access)"
msgstr "Accès gracieux"
#: users/models.py:1867
#: users/models.py:1891
msgid "You don't have the right to view other whitelists than yours."
msgstr ""
"Vous n'avez pas le droit de voir d'autres accès gracieux que les vôtres."
#: users/models.py:2065
#: users/models.py:2089
msgid "User of the local email account."
msgstr "Utilisateur du compte mail local."
#: users/models.py:2068
#: users/models.py:2092
msgid "Local part of the email address."
msgstr "Partie locale de l'adresse mail."
#: users/models.py:2073
#: users/models.py:2097
msgid "Can view a local email account object"
msgstr "Peut voir un objet compte mail local"
#: users/models.py:2075
#: users/models.py:2099
msgid "local email account"
msgstr "compte mail local"
#: users/models.py:2076
#: users/models.py:2100
msgid "local email accounts"
msgstr "comptes mail locaux"
#: users/models.py:2104 users/models.py:2139 users/models.py:2173
#: users/models.py:2207
#: users/models.py:2128 users/models.py:2163 users/models.py:2197
#: users/models.py:2231
msgid "The local email accounts are not enabled."
msgstr "Les comptes mail locaux ne sont pas activés."
#: users/models.py:2109
#: users/models.py:2133
msgid "You don't have the right to add a local email account to another user."
msgstr ""
"Vous n'avez pas le droit d'ajouter un compte mail local à un autre "
"utilisateur."
#: users/models.py:2119
#: users/models.py:2143
msgid "You reached the limit of {} local email accounts."
msgstr "Vous avez atteint la limite de {} comptes mail locaux."
#: users/models.py:2145
#: users/models.py:2169
msgid "You don't have the right to view another user's local email account."
msgstr ""
"Vous n'avez pas le droit de voir le compte mail local d'un autre utilisateur."
#: users/models.py:2165
#: users/models.py:2189
msgid ""
"You can't delete a local email account whose local part is the same as the "
"username."
@ -690,13 +702,13 @@ msgstr ""
"Vous ne pouvez pas supprimer un compte mail local dont la partie locale est "
"la même que le pseudo."
#: users/models.py:2179
#: users/models.py:2203
msgid "You don't have the right to delete another user's local email account."
msgstr ""
"Vous n'avez pas le droit de supprimer le compte mail local d'un autre "
"utilisateur."
#: users/models.py:2199
#: users/models.py:2223
msgid ""
"You can't edit a local email account whose local part is the same as the "
"username."
@ -704,13 +716,13 @@ msgstr ""
"Vous ne pouvez pas modifier un compte mail local dont la partie locale est "
"la même que le pseudo."
#: users/models.py:2213
#: users/models.py:2237
msgid "You don't have the right to edit another user's local email account."
msgstr ""
"Vous n'avez pas le droit de modifier le compte mail local d'un autre "
"utilisateur."
#: users/models.py:2222
#: users/models.py:2246
msgid "The local part must not contain @ or +."
msgstr "La partie locale ne doit pas contenir @ ou +."

View file

@ -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 %}
{% history_button users text=True detailed=True %}
</ul>
</div>
<div class="panel-body">