mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-27 01:13:46 +00:00
a9194b405a
Just ran `find /var/www/re2o \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i 's/Goulven/Lara/g'`.
543 lines
20 KiB
Python
543 lines
20 KiB
Python
# 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 © 2018 Gabriel Détraz
|
|
# Copyright © 2018 Lara Kermarec
|
|
# Copyright © 2018 Augustin Lemesle
|
|
# Copyright © 2018 Hugo Levy-Falk
|
|
#
|
|
# 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.
|
|
|
|
# App de gestion des statistiques pour re2o
|
|
# Gabriel Détraz
|
|
# Gplv2
|
|
"""
|
|
Vues des logs et statistiques générales.
|
|
|
|
La vue index générale affiche une selection des dernières actions,
|
|
classées selon l'importance, avec date, et user formatés.
|
|
|
|
Stats_logs renvoie l'ensemble des logs.
|
|
|
|
Les autres vues sont thématiques, ensemble des statistiques et du
|
|
nombre d'objets par models, nombre d'actions par user, etc
|
|
"""
|
|
|
|
from __future__ import unicode_literals
|
|
from itertools import chain
|
|
|
|
from django.urls import reverse
|
|
from django.shortcuts import render, redirect
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.http import Http404
|
|
from django.db.models import Count
|
|
from django.apps import apps
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from reversion.models import Revision
|
|
from reversion.models import Version, ContentType
|
|
|
|
from users.models import (
|
|
User,
|
|
ServiceUser,
|
|
School,
|
|
ListRight,
|
|
ListShell,
|
|
Ban,
|
|
Whitelist,
|
|
Adherent,
|
|
Club
|
|
)
|
|
from cotisations.models import (
|
|
Facture,
|
|
Vente,
|
|
Article,
|
|
Banque,
|
|
Paiement,
|
|
Cotisation
|
|
)
|
|
from machines.models import (
|
|
Machine,
|
|
MachineType,
|
|
IpType,
|
|
Extension,
|
|
Interface,
|
|
Domain,
|
|
IpList,
|
|
OuverturePortList,
|
|
Service,
|
|
Vlan,
|
|
Nas,
|
|
SOA,
|
|
Mx,
|
|
Ns
|
|
)
|
|
from topologie.models import (
|
|
Switch,
|
|
Port,
|
|
Room,
|
|
Stack,
|
|
ModelSwitch,
|
|
ConstructorSwitch,
|
|
AccessPoint
|
|
)
|
|
from preferences.models import GeneralOption
|
|
from re2o.views import form
|
|
from re2o.utils import (
|
|
all_whitelisted,
|
|
all_baned,
|
|
all_has_access,
|
|
all_adherent,
|
|
all_active_assigned_interfaces_count,
|
|
all_active_interfaces_count,
|
|
)
|
|
from re2o.base import (
|
|
re2o_paginator,
|
|
SortTable
|
|
)
|
|
from re2o.acl import (
|
|
can_view_all,
|
|
can_view_app,
|
|
can_edit_history,
|
|
)
|
|
|
|
|
|
@login_required
|
|
@can_view_app('logs')
|
|
def index(request):
|
|
"""Affiche les logs affinés, date reformatées, selectionne
|
|
les event importants (ajout de droits, ajout de ban/whitelist)"""
|
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
|
# The types of content kept for display
|
|
content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user']
|
|
# Select only wanted versions
|
|
versions = Version.objects.filter(
|
|
content_type__in=ContentType.objects.filter(
|
|
model__in=content_type_filter
|
|
)
|
|
).select_related('revision')
|
|
versions = SortTable.sort(
|
|
versions,
|
|
request.GET.get('col'),
|
|
request.GET.get('order'),
|
|
SortTable.LOGS_INDEX
|
|
)
|
|
versions = re2o_paginator(request, versions, pagination_number)
|
|
# Force to have a list instead of QuerySet
|
|
versions.count(0)
|
|
# Items to remove later because invalid
|
|
to_remove = []
|
|
# Parse every item (max = pagination_number)
|
|
for i in range(len(versions.object_list)):
|
|
if versions.object_list[i].object:
|
|
version = versions.object_list[i]
|
|
versions.object_list[i] = {
|
|
'rev_id': version.revision.id,
|
|
'comment': version.revision.comment,
|
|
'datetime': version.revision.date_created.strftime(
|
|
'%d/%m/%y %H:%M:%S'
|
|
),
|
|
'username':
|
|
version.revision.user.get_username()
|
|
if version.revision.user else '?',
|
|
'user_id': version.revision.user_id,
|
|
'version': version}
|
|
else:
|
|
to_remove.insert(0, i)
|
|
# Remove all tagged invalid items
|
|
for i in to_remove:
|
|
versions.object_list.pop(i)
|
|
return render(request, 'logs/index.html', {'versions_list': versions})
|
|
|
|
|
|
@login_required
|
|
@can_view_all(GeneralOption)
|
|
def stats_logs(request):
|
|
"""Affiche l'ensemble des logs et des modifications sur les objets,
|
|
classés par date croissante, en vrac"""
|
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
|
revisions = Revision.objects.all().select_related('user')\
|
|
.prefetch_related('version_set__object')
|
|
revisions = SortTable.sort(
|
|
revisions,
|
|
request.GET.get('col'),
|
|
request.GET.get('order'),
|
|
SortTable.LOGS_STATS_LOGS
|
|
)
|
|
revisions = re2o_paginator(request, revisions, pagination_number)
|
|
return render(request, 'logs/stats_logs.html', {
|
|
'revisions_list': revisions
|
|
})
|
|
|
|
|
|
@login_required
|
|
@can_edit_history
|
|
def revert_action(request, revision_id):
|
|
""" Annule l'action en question """
|
|
try:
|
|
revision = Revision.objects.get(id=revision_id)
|
|
except Revision.DoesNotExist:
|
|
messages.error(request, _("Nonexistent revision."))
|
|
if request.method == "POST":
|
|
revision.revert()
|
|
messages.success(request, _("The action was deleted."))
|
|
return redirect(reverse('logs:index'))
|
|
return form({
|
|
'objet': revision,
|
|
'objet_name': revision.__class__.__name__
|
|
}, 'logs/delete.html', request)
|
|
|
|
|
|
@login_required
|
|
@can_view_all(IpList, Interface, User)
|
|
def stats_general(request):
|
|
"""Statistiques générales affinées sur les ip, activées, utilisées par
|
|
range, et les statistiques générales sur les users : users actifs,
|
|
cotisants, activés, archivés, etc"""
|
|
ip_dict = dict()
|
|
for ip_range in IpType.objects.select_related('vlan').all():
|
|
all_ip = IpList.objects.filter(ip_type=ip_range)
|
|
used_ip = Interface.objects.filter(ipv4__in=all_ip).count()
|
|
active_ip = all_active_assigned_interfaces_count().filter(
|
|
ipv4__in=IpList.objects.filter(ip_type=ip_range)
|
|
).count()
|
|
ip_dict[ip_range] = [ip_range, ip_range.vlan, all_ip.count(),
|
|
used_ip, active_ip, all_ip.count()-used_ip]
|
|
_all_adherent = all_adherent(including_asso=False)
|
|
_all_has_access = all_has_access(including_asso=False)
|
|
_all_baned = all_baned()
|
|
_all_whitelisted = all_whitelisted()
|
|
_all_active_interfaces_count = all_active_interfaces_count()
|
|
_all_active_assigned_interfaces_count = \
|
|
all_active_assigned_interfaces_count()
|
|
stats = [
|
|
[ # First set of data (about users)
|
|
[ # Headers
|
|
_("Category"),
|
|
_("Number of users (members and clubs)"),
|
|
_("Number of members"),
|
|
_("Number of clubs")
|
|
],
|
|
{ # Data
|
|
'active_users': [
|
|
_("Activated users"),
|
|
User.objects.filter(state=User.STATE_ACTIVE).count(),
|
|
(Adherent.objects
|
|
.filter(state=Adherent.STATE_ACTIVE)
|
|
.count()),
|
|
Club.objects.filter(state=Club.STATE_ACTIVE).count()
|
|
],
|
|
'inactive_users': [
|
|
_("Disabled users"),
|
|
User.objects.filter(state=User.STATE_DISABLED).count(),
|
|
(Adherent.objects
|
|
.filter(state=Adherent.STATE_DISABLED)
|
|
.count()),
|
|
Club.objects.filter(state=Club.STATE_DISABLED).count()
|
|
],
|
|
'archive_users': [
|
|
_("Archived users"),
|
|
User.objects.filter(state=User.STATE_ARCHIVE).count(),
|
|
(Adherent.objects
|
|
.filter(state=Adherent.STATE_ARCHIVE)
|
|
.count()),
|
|
Club.objects.filter(state=Club.STATE_ARCHIVE).count()
|
|
],
|
|
'full_archive_users': [
|
|
_("Full Archived users"),
|
|
User.objects.filter(state=User.STATE_FULL_ARCHIVE).count(),
|
|
(Adherent.objects
|
|
.filter(state=Adherent.STATE_FULL_ARCHIVE)
|
|
.count()),
|
|
Club.objects.filter(state=Club.STATE_FULL_ARCHIVE).count()
|
|
],
|
|
'not_active_users': [
|
|
_("Not yet active users"),
|
|
User.objects.filter(state=User.STATE_NOT_YET_ACTIVE).count(),
|
|
(Adherent.objects
|
|
.filter(state=Adherent.STATE_NOT_YET_ACTIVE)
|
|
.count()),
|
|
Club.objects.filter(state=Club.STATE_NOT_YET_ACTIVE).count()
|
|
],
|
|
'adherent_users': [
|
|
_("Contributing members"),
|
|
_all_adherent.count(),
|
|
_all_adherent.exclude(adherent__isnull=True).count(),
|
|
_all_adherent.exclude(club__isnull=True).count()
|
|
],
|
|
'connexion_users': [
|
|
_("Users benefiting from a connection"),
|
|
_all_has_access.count(),
|
|
_all_has_access.exclude(adherent__isnull=True).count(),
|
|
_all_has_access.exclude(club__isnull=True).count()
|
|
],
|
|
'ban_users': [
|
|
_("Banned users"),
|
|
_all_baned.count(),
|
|
_all_baned.exclude(adherent__isnull=True).count(),
|
|
_all_baned.exclude(club__isnull=True).count()
|
|
],
|
|
'whitelisted_user': [
|
|
_("Users benefiting from a free connection"),
|
|
_all_whitelisted.count(),
|
|
_all_whitelisted.exclude(adherent__isnull=True).count(),
|
|
_all_whitelisted.exclude(club__isnull=True).count()
|
|
],
|
|
'actives_interfaces': [
|
|
_("Active interfaces (with access to the network)"),
|
|
_all_active_interfaces_count.count(),
|
|
(_all_active_interfaces_count
|
|
.exclude(machine__user__adherent__isnull=True)
|
|
.count()),
|
|
(_all_active_interfaces_count
|
|
.exclude(machine__user__club__isnull=True)
|
|
.count())
|
|
],
|
|
'actives_assigned_interfaces': [
|
|
_("Active interfaces assigned IPv4"),
|
|
_all_active_assigned_interfaces_count.count(),
|
|
(_all_active_assigned_interfaces_count
|
|
.exclude(machine__user__adherent__isnull=True)
|
|
.count()),
|
|
(_all_active_assigned_interfaces_count
|
|
.exclude(machine__user__club__isnull=True)
|
|
.count())
|
|
]
|
|
}
|
|
],
|
|
[ # Second set of data (about ip adresses)
|
|
[ # Headers
|
|
_("IP range"),
|
|
_("VLAN"),
|
|
_("Total number of IP addresses"),
|
|
_("Number of assigned IP addresses"),
|
|
_("Number of IP address assigned to an activated machine"),
|
|
_("Number of nonassigned IP addresses")
|
|
],
|
|
ip_dict # Data already prepared
|
|
]
|
|
]
|
|
return render(request, 'logs/stats_general.html', {'stats_list': stats})
|
|
|
|
|
|
@login_required
|
|
@can_view_app('users', 'cotisations', 'machines', 'topologie')
|
|
def stats_models(request):
|
|
"""Statistiques générales, affiche les comptages par models:
|
|
nombre d'users, d'écoles, de droits, de bannissements,
|
|
de factures, de ventes, de banque, de machines, etc"""
|
|
stats = {
|
|
_("Users"): {
|
|
'users': [User._meta.verbose_name, User.objects.count()],
|
|
'adherents': [Adherent._meta.verbose_name, Adherent.objects.count()],
|
|
'clubs': [Club._meta.verbose_name, Club.objects.count()],
|
|
'serviceuser': [ServiceUser._meta.verbose_name,
|
|
ServiceUser.objects.count()],
|
|
'school': [School._meta.verbose_name, School.objects.count()],
|
|
'listright': [ListRight._meta.verbose_name, ListRight.objects.count()],
|
|
'listshell': [ListShell._meta.verbose_name, ListShell.objects.count()],
|
|
'ban': [Ban._meta.verbose_name, Ban.objects.count()],
|
|
'whitelist': [Whitelist._meta.verbose_name, Whitelist.objects.count()]
|
|
},
|
|
_("Subscriptions"): {
|
|
'factures': [
|
|
Facture._meta.verbose_name,
|
|
Facture.objects.count()
|
|
],
|
|
'vente': [
|
|
Vente._meta.verbose_name,
|
|
Vente.objects.count()
|
|
],
|
|
'cotisation': [
|
|
Cotisation._meta.verbose_name,
|
|
Cotisation.objects.count()
|
|
],
|
|
'article': [
|
|
Article._meta.verbose_name,
|
|
Article.objects.count()
|
|
],
|
|
'banque': [
|
|
Banque._meta.verbose_name,
|
|
Banque.objects.count()
|
|
],
|
|
},
|
|
_("Machines"): {
|
|
'machine': [Machine._meta.verbose_name,
|
|
Machine.objects.count()],
|
|
'typemachine': [MachineType._meta.verbose_name,
|
|
MachineType.objects.count()],
|
|
'typeip': [IpType._meta.verbose_name,
|
|
IpType.objects.count()],
|
|
'extension': [Extension._meta.verbose_name,
|
|
Extension.objects.count()],
|
|
'interface': [Interface._meta.verbose_name,
|
|
Interface.objects.count()],
|
|
'alias': [Domain._meta.verbose_name,
|
|
Domain.objects.exclude(cname=None).count()],
|
|
'iplist': [IpList._meta.verbose_name,
|
|
IpList.objects.count()],
|
|
'service': [Service._meta.verbose_name,
|
|
Service.objects.count()],
|
|
'ouvertureportlist': [
|
|
OuverturePortList._meta.verbose_name,
|
|
OuverturePortList.objects.count()
|
|
],
|
|
'vlan': [Vlan._meta.verbose_name, Vlan.objects.count()],
|
|
'SOA': [SOA._meta.verbose_name, SOA.objects.count()],
|
|
'Mx': [Mx._meta.verbose_name, Mx.objects.count()],
|
|
'Ns': [Ns._meta.verbose_name, Ns.objects.count()],
|
|
'nas': [Nas._meta.verbose_name, Nas.objects.count()],
|
|
},
|
|
_("Topology"): {
|
|
'switch': [Switch._meta.verbose_name,
|
|
Switch.objects.count()],
|
|
'bornes': [AccessPoint._meta.verbose_name,
|
|
AccessPoint.objects.count()],
|
|
'port': [Port._meta.verbose_name, Port.objects.count()],
|
|
'chambre': [Room._meta.verbose_name, Room.objects.count()],
|
|
'stack': [Stack._meta.verbose_name, Stack.objects.count()],
|
|
'modelswitch': [
|
|
ModelSwitch._meta.verbose_name,
|
|
ModelSwitch.objects.count()
|
|
],
|
|
'constructorswitch': [
|
|
ConstructorSwitch._meta.verbose_name,
|
|
ConstructorSwitch.objects.count()
|
|
],
|
|
},
|
|
_("Actions performed"):
|
|
{
|
|
'revision': [_("Number of actions"), Revision.objects.count()],
|
|
},
|
|
}
|
|
return render(request, 'logs/stats_models.html', {'stats_list': stats})
|
|
|
|
|
|
@login_required
|
|
@can_view_app('users')
|
|
def stats_users(request):
|
|
"""Affiche les statistiques base de données aggrégées par user :
|
|
nombre de machines par user, d'etablissements par user,
|
|
de moyens de paiements par user, de banque par user,
|
|
de bannissement par user, etc"""
|
|
stats = {
|
|
_("User"): {
|
|
_("Machines"): User.objects.annotate(
|
|
num=Count('machine')
|
|
).order_by('-num')[:10],
|
|
_("Invoice"): User.objects.annotate(
|
|
num=Count('facture')
|
|
).order_by('-num')[:10],
|
|
_("Ban"): User.objects.annotate(
|
|
num=Count('ban')
|
|
).order_by('-num')[:10],
|
|
_("Whitelist"): User.objects.annotate(
|
|
num=Count('whitelist')
|
|
).order_by('-num')[:10],
|
|
_("Rights"): User.objects.annotate(
|
|
num=Count('groups')
|
|
).order_by('-num')[:10],
|
|
},
|
|
_("School"): {
|
|
_("User"): School.objects.annotate(
|
|
num=Count('user')
|
|
).order_by('-num')[:10],
|
|
},
|
|
_("Payment method"): {
|
|
_("User"): Paiement.objects.annotate(
|
|
num=Count('facture')
|
|
).order_by('-num')[:10],
|
|
},
|
|
_("Bank"): {
|
|
_("User"): Banque.objects.annotate(
|
|
num=Count('facture')
|
|
).order_by('-num')[:10],
|
|
},
|
|
}
|
|
return render(request, 'logs/stats_users.html', {'stats_list': stats})
|
|
|
|
|
|
@login_required
|
|
@can_view_app('users')
|
|
def stats_actions(request):
|
|
"""Vue qui affiche les statistiques de modifications d'objets par
|
|
utilisateurs.
|
|
Affiche le nombre de modifications aggrégées par utilisateurs"""
|
|
stats = {
|
|
_("User"): {
|
|
_("Action"): User.objects.annotate(
|
|
num=Count('revision')
|
|
).order_by('-num')[:40],
|
|
},
|
|
}
|
|
return render(request, 'logs/stats_users.html', {'stats_list': stats})
|
|
|
|
|
|
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.
|
|
|
|
Args:
|
|
request: The request sent by the user.
|
|
application: Name of the application.
|
|
object_name: Name of the model.
|
|
object_id: Id of the object you want to acces history.
|
|
|
|
Returns:
|
|
The rendered page of history if access is granted, else the user is
|
|
redirected to their profile page, with an error message.
|
|
|
|
Raises:
|
|
Http404: This kind of models doesn't have history.
|
|
"""
|
|
try:
|
|
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)}
|
|
))
|
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
|
reversions = Version.objects.get_for_object(instance)
|
|
if hasattr(instance, 'linked_objects'):
|
|
for related_object in chain(instance.linked_objects()):
|
|
reversions = (reversions |
|
|
Version.objects.get_for_object(related_object))
|
|
reversions = re2o_paginator(request, reversions, pagination_number)
|
|
return render(
|
|
request,
|
|
're2o/history.html',
|
|
{'reversions': reversions, 'object': instance}
|
|
)
|
|
|