# 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 Goulven 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, re2o_paginator, ) from re2o.acl import ( can_view_all, can_view_app, can_edit_history, ) from re2o.utils import all_active_assigned_interfaces_count from re2o.utils import all_active_interfaces_count, SortTable @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() _all_has_access = all_has_access() _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() ], '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 = 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} ) @login_required @can_view_app('users') def stats_rooms(request): """Vue qui affiche les chambres inoccupées pour des fins de contrôle des cotisations""" stats = { _("Unoccupied rooms count"): { _("Action"): Room.objects.filter(adherent__isnull=True).count() }, _("Unoccupied rooms"): { _("Action"): Room.objects.filter( adherent__isnull=True ).order_by('-num')[:40], }, } return render(request, 'logs/stats_rooms.html', {'stats_list': stats})