8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-07-04 05:04:06 +00:00

Merge branch 'machine_history' into 'dev'

Add machine history view

See merge request re2o/re2o!513
This commit is contained in:
klafyvel 2020-04-22 23:30:26 +02:00
commit 623df4a9c3
19 changed files with 521 additions and 49 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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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"

55
logs/forms.py Normal file
View file

@ -0,0 +1,55 @@
# 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.
"""The forms used by the search app"""
from django import forms
from django.forms import Form
from django.utils.translation import ugettext_lazy as _
from re2o.base import get_input_formats_help_text
CHOICES_TYPE = (
("ip", _("IPv4")),
("mac", _("MAC address")),
)
class MachineHistoryForm(Form):
"""The form for a simple search"""
q = forms.CharField(
label=_("Search"),
max_length=100,
)
t = forms.CharField(
label=_("Search type"),
widget=forms.Select(choices=CHOICES_TYPE)
)
s = forms.DateField(required=False, label=_("Start date"))
e = forms.DateField(required=False, label=_("End date"))
def __init__(self, *args, **kwargs):
super(MachineHistoryForm, self).__init__(*args, **kwargs)
self.fields["s"].help_text = get_input_formats_help_text(
self.fields["s"].input_formats
)
self.fields["e"].help_text = get_input_formats_help_text(
self.fields["e"].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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+0200\n"
"PO-Revision-Date: 2018-06-23 16:01+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -34,6 +34,30 @@ msgstr ""
msgid "You don't have the right to view this application."
msgstr "Vous n'avez pas le droit de voir cette application."
#: logs/forms.py:29 logs/templates/logs/machine_history.html:35
msgid "IPv4"
msgstr "IPv4"
#: logs/forms.py:30 logs/templates/logs/machine_history.html:36
msgid "MAC address"
msgstr "Adresse MAC"
#: logs/forms.py:38 logs/templates/logs/search_machine_history.html:38
msgid "Search"
msgstr "Rechercher"
#: logs/forms.py:42
msgid "Search type"
msgstr "Type de recherche"
#: logs/forms.py:45 logs/templates/logs/machine_history.html:37
msgid "Start date"
msgstr "Date de début"
#: logs/forms.py:46 logs/templates/logs/machine_history.html:38
msgid "End date"
msgstr "Date de fin"
#: logs/templates/logs/aff_stats_logs.html:36
msgid "Edited object"
msgstr "Objet modifié"
@ -52,6 +76,7 @@ msgid "Date of editing"
msgstr "Date de modification"
#: logs/templates/logs/aff_stats_logs.html:42
#: logs/templates/logs/machine_history.html:39
msgid "Comment"
msgstr "Commentaire"
@ -159,10 +184,35 @@ msgid "Statistics"
msgstr "Statistiques"
#: logs/templates/logs/index.html:32 logs/templates/logs/stats_logs.html:32
#: logs/views.py:418
#: logs/views.py:421
msgid "Actions performed"
msgstr "Actions effectuées"
#: logs/templates/logs/machine_history.html:27
msgid "Search results"
msgstr "Résultats de la recherche"
#: logs/templates/logs/machine_history.html:34
msgid "User"
msgstr "Utilisateur"
#: logs/templates/logs/machine_history.html:55
msgid "Unknown"
msgstr "Inconnu(e)"
#: logs/templates/logs/machine_history.html:62
msgid "Now"
msgstr "Maintenant"
#: logs/templates/logs/machine_history.html:70
msgid "No result"
msgstr "Aucun résultat"
#: logs/templates/logs/search_machine_history.html:27
#: logs/templates/logs/search_machine_history.html:32
msgid "Search machine history"
msgstr "Rechercher l'historique des machines"
#: logs/templates/logs/sidebar.html:33
msgid "Summary"
msgstr "Résumé"
@ -187,6 +237,10 @@ msgstr "Actions de câblage"
msgid "Users"
msgstr "Utilisateurs"
#: logs/templates/logs/sidebar.html:57
msgid "Machine history"
msgstr "Historique des machines"
#: logs/templates/logs/stats_general.html:32
msgid "General statistics"
msgstr "Statistiques générales"
@ -199,138 +253,138 @@ msgstr "Statistiques sur la base de données"
msgid "Statistics about users"
msgstr "Statistiques sur les utilisateurs"
#: logs/views.py:175
#: logs/views.py:178
msgid "Nonexistent revision."
msgstr "Révision inexistante."
#: logs/views.py:178
#: logs/views.py:181
msgid "The action was deleted."
msgstr "L'action a été supprimée."
#: logs/views.py:219
#: logs/views.py:222
msgid "Category"
msgstr "Catégorie"
#: logs/views.py:220
#: logs/views.py:223
msgid "Number of users (members and clubs)"
msgstr "Nombre d'utilisateurs (adhérents et clubs)"
#: logs/views.py:221
#: logs/views.py:224
msgid "Number of members"
msgstr "Nombre d'adhérents"
#: logs/views.py:222
#: logs/views.py:225
msgid "Number of clubs"
msgstr "Nombre de clubs"
#: logs/views.py:226
#: logs/views.py:229
msgid "Activated users"
msgstr "Utilisateurs activés"
#: logs/views.py:232
#: logs/views.py:235
msgid "Disabled users"
msgstr "Utilisateurs désactivés"
#: logs/views.py:238
#: logs/views.py:241
msgid "Archived users"
msgstr "Utilisateurs archivés"
#: logs/views.py:244
#: logs/views.py:247
msgid "Fully archived users"
msgstr "Utilisateurs complètement archivés"
#: logs/views.py:254
#: logs/views.py:257
msgid "Not yet active users"
msgstr "Utilisateurs pas encore actifs"
#: logs/views.py:264
#: logs/views.py:267
msgid "Contributing members"
msgstr "Adhérents cotisants"
#: logs/views.py:270
#: logs/views.py:273
msgid "Users benefiting from a connection"
msgstr "Utilisateurs bénéficiant d'une connexion"
#: logs/views.py:276
#: logs/views.py:279
msgid "Banned users"
msgstr "Utilisateurs bannis"
#: logs/views.py:282
#: logs/views.py:285
msgid "Users benefiting from a free connection"
msgstr "Utilisateurs bénéficiant d'une connexion gratuite"
#: logs/views.py:288
#: logs/views.py:291
msgid "Users with a confirmed email"
msgstr "Utilisateurs ayant un mail confirmé"
#: logs/views.py:294
#: logs/views.py:297
msgid "Users with an unconfirmed email"
msgstr "Utilisateurs ayant un mail non confirmé"
#: logs/views.py:300
#: logs/views.py:303
msgid "Users pending email confirmation"
msgstr "Utilisateurs en attente de confirmation du mail"
#: logs/views.py:306
#: logs/views.py:309
msgid "Active interfaces (with access to the network)"
msgstr "Interfaces actives (ayant accès au réseau)"
#: logs/views.py:320
#: logs/views.py:323
msgid "Active interfaces assigned IPv4"
msgstr "Interfaces actives assignées IPv4"
#: logs/views.py:337
#: logs/views.py:340
msgid "IP range"
msgstr "Plage d'IP"
#: logs/views.py:338
#: logs/views.py:341
msgid "VLAN"
msgstr "VLAN"
#: logs/views.py:339
#: logs/views.py:342
msgid "Total number of IP addresses"
msgstr "Nombre total d'adresses IP"
#: logs/views.py:340
#: logs/views.py:343
msgid "Number of assigned IP addresses"
msgstr "Nombre d'adresses IP assignées"
#: logs/views.py:341
#: logs/views.py:344
msgid "Number of IP address assigned to an activated machine"
msgstr "Nombre d'adresses IP assignées à une machine activée"
#: logs/views.py:342
#: logs/views.py:345
msgid "Number of unassigned IP addresses"
msgstr "Nombre d'adresses IP non assignées"
#: logs/views.py:357
#: logs/views.py:360
msgid "Users (members and clubs)"
msgstr "Utilisateurs (adhérents et clubs)"
#: logs/views.py:403
#: logs/views.py:406
msgid "Topology"
msgstr "Topologie"
#: logs/views.py:419
#: logs/views.py:422
msgid "Number of actions"
msgstr "Nombre d'actions"
#: logs/views.py:444
#: logs/views.py:447
msgid "rights"
msgstr "droits"
#: logs/views.py:473
#: logs/views.py:476
msgid "actions"
msgstr "actions"
#: logs/views.py:504
#: logs/views.py:529
msgid "No model found."
msgstr "Aucun modèle trouvé."
#: logs/views.py:510
#: logs/views.py:535
msgid "Nonexistent entry."
msgstr "Entrée inexistante."
#: logs/views.py:517
#: logs/views.py:542
msgid "You don't have the right to access this menu."
msgstr "Vous n'avez pas le droit d'accéder à ce menu."

207
logs/models.py Normal file
View file

@ -0,0 +1,207 @@
# -*- mode: python; coding: utf-8 -*-
# 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.
"""logs.models
The models definitions for the logs app
"""
from reversion.models import Version
from machines.models import IpList
from machines.models import Interface
from machines.models import Machine
from users.models import User
class HistoryEvent:
def __init__(self, user, machine, interface, start=None, end=None):
"""
: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
:param start: datetime, the date at which this version was created
:param end: datetime, the date at which this version was replace by a new one
"""
self.user = user
self.machine = machine
self.interface = interface
self.ipv4 = IpList.objects.get(id=interface.field_dict["ipv4_id"]).ipv4
self.mac = self.interface.field_dict["mac_address"]
self.start_date = start
self.end_date = end
self.comment = interface.revision.get_comment() or None
def is_similar(self, elt2):
"""
Checks whether two events are similar enough to be merged
:return: bool
"""
return (
elt2 is not None
and self.user.id == elt2.user.id
and self.ipv4 == elt2.ipv4
and self.machine.field_dict["id"] == elt2.machine.field_dict["id"]
and self.interface.field_dict["id"] == elt2.interface.field_dict["id"]
)
def __repr__(self):
return "{} ({} - ): from {} to {} ({})".format(
self.machine,
self.mac,
self.ipv4,
self.start_date,
self.end_date,
self.comment or "No comment"
)
class MachineHistory:
def __init__(self):
self.events = []
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 HistoryEvent
"""
self.start = params.get("s", None)
self.end = params.get("e", None)
search_type = params.get("t", 0)
self.events = []
if search_type == "ip":
return self.__get_by_ip(search)
elif search_type == "mac":
return self.__get_by_mac(search)
return None
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 = HistoryEvent(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):
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 the event ends before the given date, remove it
if self.start and evt.start_date.date() < self.start:
self.__last_evt = None
self.events.pop()
# Make sure the new event starts before the given end date
if self.end and evt.start_date.date() > self.end:
return
# Save the new element
self.events.append(evt)
self.__last_evt = evt
def __get_interfaces_for_ip(self, ip):
"""
:param ip: str
:return: An iterable object with the Version objects
of Interfaces with the given IP
"""
# TODO: What if ip list was deleted?
try:
ip_id = IpList.objects.get(ipv4=ip).id
except IpList.DoesNotExist:
return []
return filter(
lambda x: x.field_dict["ipv4_id"] == ip_id,
Version.objects.get_for_model(Interface).order_by("revision__date_created")
)
def __get_interfaces_for_mac(self, mac):
"""
:param mac: str
:return: An iterable object with the Version objects
of Interfaces with the given MAC address
"""
return filter(
lambda x: str(x.field_dict["mac_address"]) == mac,
Version.objects.get_for_model(Interface).order_by("revision__date_created")
)
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
which the given interface was attributed
"""
machine_id = interface.field_dict["machine_id"]
return filter(
lambda x: x.field_dict["id"] == machine_id,
Version.objects.get_for_model(Machine).order_by("revision__date_created")
)
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
"""
# TODO: What if user was deleted?
user_id = machine.field_dict["user_id"]
return User.objects.get(id=user_id)
def __get_by_ip(self, ip):
"""
:param ip: str, The IP to lookup
:returns: list, a list of HistoryEvent
"""
interfaces = self.__get_interfaces_for_ip(ip)
for interface in interfaces:
machines = self.__get_machines_for_interface(interface)
for machine in machines:
user = self.__get_user_for_machine(machine)
self.__add_revision(user, machine, interface)
return self.events
def __get_by_mac(self, mac):
"""
:param mac: str, The MAC address to lookup
:returns: list, a list of HistoryEvent
"""
interfaces = self.__get_interfaces_for_mac(mac)
for interface in interfaces:
machines = self.__get_machines_for_interface(interface)
for machine in machines:
user = self.__get_user_for_machine(machine)
self.__add_revision(user, machine, interface)
return self.events

View file

@ -0,0 +1,76 @@
{% 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 "Search results" %}{% endblock %}
{% block content %}
{% if events %}
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "IPv4" %}</th>
<th>{% trans "MAC address" %}</th>
<th>{% trans "Start date" %}</th>
<th>{% trans "End date" %}</th>
<th>{% trans "Comment" %}</th>
</tr>
</thead>
{% for event in events %}
<tr>
<td>
<a href="{% url 'users:profil' userid=event.user.id %}" title=tr_view_the_profile>
{{ event.user }}
</a>
</td>
<td>{{ event.ipv4 }}</td>
<td>{{ event.mac }}</td>
<td>
{% if event.start_date %}
{{ event.start_date }}
{% else %}
{% trans "Unknown" %}
{% endif %}
</td>
<td>
{% if event.end_date %}
{{ event.end_date }}
{% else %}
{% trans "Now" %}
{% endif %}
</td>
<td>{{ event.comment }}</td>
</tr>
{% endfor %}
</table>
{% include 'pagination.html' with list=events %}
{% else %}
<h3>{% trans "No result" %}</h3>
{% endif %}
<br />
<br />
<br />
{% endblock %}

View file

@ -0,0 +1,46 @@
{% 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 "Search machine history" %}{% endblock %}
{% block content %}
<form class="form">
<h3>{% trans "Search machine history" %}</h3>
{% bootstrap_field history_form.q %}
{% bootstrap_field history_form.t %}
{% bootstrap_field history_form.s %}
{% bootstrap_field history_form.e %}
{% trans "Search" as tr_search %}
{% bootstrap_button tr_search button_type="submit" icon="search" %}
</form>
<br />
<br />
<br />
<br />
<br />
{% endblock %}

View file

@ -52,6 +52,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-users"></i>
{% trans "Users" %}
</a>
<a class="list-group-item list-group-item-info" href="{% url 'logs:stats-search-machine' %}">
<i class="fa fa-desktop"></i>
{% trans "Machine history" %}
</a>
{% acl_end %}
{% endblock %}

View file

@ -46,4 +46,5 @@ urlpatterns = [
views.history,
name="history",
),
url(r"^stats_search_machine/$", views.stats_search_machine_history, name="stats-search-machine"),
]

View file

@ -101,6 +101,9 @@ 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 .forms import MachineHistoryForm
@login_required
@can_view_app("logs")
@ -478,6 +481,33 @@ def stats_actions(request):
return render(request, "logs/stats_users.html", {"stats_list": stats})
@login_required
@can_view_app("users")
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)
if history_form.is_valid():
history = MachineHistory()
events = history.get(
history_form.cleaned_data.get("q", ""),
history_form.cleaned_data
)
max_result = GeneralOption.get_cached_value("pagination_number")
events = re2o_paginator(
request,
events,
max_result
)
return render(
request,
"logs/machine_history.html",
{ "events": events },
)
return render(request, "logs/search_machine_history.html", {"history_form": history_form})
def history(request, application, object_name, object_id):
"""Render history for a model.

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-21 22:29+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+0200\n"
"PO-Revision-Date: 2018-06-24 15:54+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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+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-21 21:38+0200\n"
"POT-Creation-Date: 2020-04-22 19:00+0200\n"
"PO-Revision-Date: 2018-06-27 23:35+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -492,8 +492,7 @@ msgstr "Le champ mail ne peut pas ^êêtre vide"
#: users/models.py:1344
msgid "You can't use a {} address as an external contact address."
msgstr ""
"Vous ne pouvez pas utiliser une adresse {} pour votre adresse externe."
msgstr "Vous ne pouvez pas utiliser une adresse {} pour votre adresse externe."
#: users/models.py:1371
msgid "member"