views.py 16.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 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 © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
#
# 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.

23 24 25
# App de gestion des statistiques pour re2o
# Gabriel Détraz
# Gplv2
chirac's avatar
chirac committed
26 27 28 29 30 31 32 33 34 35 36
"""
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
"""
37 38 39

from __future__ import unicode_literals

40
from django.urls import reverse
41 42 43
from django.shortcuts import render, redirect
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib import messages
44
from django.contrib.auth.decorators import login_required
chirac's avatar
chirac committed
45
from django.db.models import Count
46
from django.db.models import Max
47 48

from reversion.models import Revision
49
from reversion.models import Version, ContentType
50

51 52
from time import time

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
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
)
96
from preferences.models import GeneralOption
chirac's avatar
chirac committed
97
from re2o.views import form
98 99 100 101 102
from re2o.utils import (
    all_whitelisted,
    all_baned,
    all_has_access,
    all_adherent,
103 104
)
from re2o.acl import (
105 106 107 108
    can_view_all,
    can_view_app,
    can_edit_history,
)
chirac's avatar
chirac committed
109
from re2o.utils import all_active_assigned_interfaces_count
110
from re2o.utils import all_active_interfaces_count, SortTable
guimoz's avatar
guimoz committed
111 112

STATS_DICT = {
chirac's avatar
chirac committed
113 114 115 116 117 118
    0: ["Tout", 36],
    1: ["1 mois", 1],
    2: ["2 mois", 2],
    3: ["6 mois", 6],
    4: ["1 an", 12],
    5: ["2 an", 24],
guimoz's avatar
guimoz committed
119 120
}

121

122
@login_required
123
@can_view_app('logs')
124
def index(request):
chirac's avatar
chirac committed
125 126
    """Affiche les logs affinés, date reformatées, selectionne
    les event importants (ajout de droits, ajout de ban/whitelist)"""
127
    pagination_number = GeneralOption.get_cached_value('pagination_number')
128
    # The types of content kept for display
chirac's avatar
chirac committed
129
    content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user']
130
    # Select only wanted versions
chirac's avatar
chirac committed
131 132 133 134
    versions = Version.objects.filter(
        content_type__in=ContentType.objects.filter(
            model__in=content_type_filter
        )
135 136 137 138 139 140 141
    ).select_related('revision')
    versions = SortTable.sort(
        versions,
        request.GET.get('col'),
        request.GET.get('order'),
        SortTable.LOGS_INDEX
    )
142
    paginator = Paginator(versions, pagination_number)
143 144
    page = request.GET.get('page')
    try:
145
        versions = paginator.page(page)
146 147
    except PageNotAnInteger:
        # If page is not an integer, deliver first page.
148
        versions = paginator.page(1)
149
    except EmptyPage:
chirac's avatar
chirac committed
150
        # If page is out of range (e.g. 9999), deliver last page of results.
151 152 153 154 155 156 157
        versions = paginator.page(paginator.num_pages)

    # 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)
chirac's avatar
chirac committed
158 159 160
    for i in range(len(versions.object_list)):
        if versions.object_list[i].object:
            version = versions.object_list[i]
161
            versions.object_list[i] = {
chirac's avatar
chirac committed
162 163 164 165 166 167 168 169 170 171 172 173
                '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)
174
    # Remove all tagged invalid items
chirac's avatar
chirac committed
175
    for i in to_remove:
176 177
        versions.object_list.pop(i)
    return render(request, 'logs/index.html', {'versions_list': versions})
178

chirac's avatar
chirac committed
179

180
@login_required
181
@can_view_all(GeneralOption)
182
def stats_logs(request):
chirac's avatar
chirac committed
183 184
    """Affiche l'ensemble des logs et des modifications sur les objets,
    classés par date croissante, en vrac"""
185
    pagination_number = GeneralOption.get_cached_value('pagination_number')
186
    revisions = Revision.objects.all().select_related('user')\
chirac's avatar
chirac committed
187
        .prefetch_related('version_set__object')
188 189 190 191 192 193
    revisions = SortTable.sort(
        revisions,
        request.GET.get('col'),
        request.GET.get('order'),
        SortTable.LOGS_STATS_LOGS
    )
194 195 196 197 198 199 200 201
    paginator = Paginator(revisions, pagination_number)
    page = request.GET.get('page')
    try:
        revisions = paginator.page(page)
    except PageNotAnInteger:
        # If page is not an integer, deliver first page.
        revisions = paginator.page(1)
    except EmptyPage:
chirac's avatar
chirac committed
202
        # If page is out of range (e.g. 9999), deliver last page of results.
203
        revisions = paginator.page(paginator.num_pages)
chirac's avatar
chirac committed
204 205 206 207
    return render(request, 'logs/stats_logs.html', {
        'revisions_list': revisions
        })

208

209
@login_required
210
@can_edit_history
211 212 213 214 215
def revert_action(request, revision_id):
    """ Annule l'action en question """
    try:
        revision = Revision.objects.get(id=revision_id)
    except Revision.DoesNotExist:
chirac's avatar
chirac committed
216
        messages.error(request, u"Revision inexistante")
217 218 219
    if request.method == "POST":
        revision.revert()
        messages.success(request, "L'action a été supprimée")
220
        return redirect(reverse('logs:index'))
chirac's avatar
chirac committed
221 222 223 224 225
    return form({
        'objet': revision,
        'objet_name': revision.__class__.__name__
        }, 'logs/delete.html', request)

226

Gabriel Detraz's avatar
Gabriel Detraz committed
227
@login_required
228 229 230
@can_view_all(IpList)
@can_view_all(Interface)
@can_view_all(User)
Gabriel Detraz's avatar
Gabriel Detraz committed
231
def stats_general(request):
chirac's avatar
chirac committed
232 233 234 235
    """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()
236
    for ip_range in IpType.objects.select_related('vlan').all():
Gabriel Detraz's avatar
Gabriel Detraz committed
237 238
        all_ip = IpList.objects.filter(ip_type=ip_range)
        used_ip = Interface.objects.filter(ipv4__in=all_ip).count()
chirac's avatar
chirac committed
239 240 241
        active_ip = all_active_assigned_interfaces_count().filter(
            ipv4__in=IpList.objects.filter(ip_type=ip_range)
        ).count()
242
        ip_dict[ip_range] = [ip_range, ip_range.vlan, all_ip.count(),
chirac's avatar
chirac committed
243
                             used_ip, active_ip, all_ip.count()-used_ip]
244 245 246 247 248 249
    _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()
Gabriel Detraz's avatar
Gabriel Detraz committed
250
    stats = [
251
        [["Categorie", "Nombre d'utilisateurs (total club et adhérents)", "Nombre d'adhérents", "Nombre de clubs"], {
chirac's avatar
chirac committed
252 253
            'active_users': [
                "Users actifs",
254 255 256 257
                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': [
chirac's avatar
chirac committed
258
                "Users désactivés",
259 260 261
                User.objects.filter(state=User.STATE_DISABLED).count(),
                Adherent.objects.filter(state=Adherent.STATE_DISABLED).count(),
                Club.objects.filter(state=Club.STATE_DISABLED).count()],
chirac's avatar
chirac committed
262 263
            'archive_users': [
                "Users archivés",
264 265 266
                User.objects.filter(state=User.STATE_ARCHIVE).count(),
                Adherent.objects.filter(state=Adherent.STATE_ARCHIVE).count(),
                Club.objects.filter(state=Club.STATE_ARCHIVE).count()],
chirac's avatar
chirac committed
267
            'adherent_users': [
268 269 270 271
                "Cotisant à l'association",
                _all_adherent.count(),
                _all_adherent.exclude(adherent__isnull=True).count(),
                _all_adherent.exclude(club__isnull=True).count()],
chirac's avatar
chirac committed
272 273
            'connexion_users': [
                "Utilisateurs bénéficiant d'une connexion",
274 275 276
                _all_has_access.count(),
                _all_has_access.exclude(adherent__isnull=True).count(),
                _all_has_access.exclude(club__isnull=True).count()],
chirac's avatar
chirac committed
277 278
            'ban_users': [
                "Utilisateurs bannis",
279 280 281
                _all_baned.count(),
                _all_baned.exclude(adherent__isnull=True).count(),
                _all_baned.exclude(club__isnull=True).count()],
chirac's avatar
chirac committed
282 283
            'whitelisted_user': [
                "Utilisateurs bénéficiant d'une connexion gracieuse",
284 285 286
                _all_whitelisted.count(),
                _all_whitelisted.exclude(adherent__isnull=True).count(),
                _all_whitelisted.exclude(club__isnull=True).count()],
chirac's avatar
chirac committed
287 288
            'actives_interfaces': [
                "Interfaces actives (ayant accès au reseau)",
289 290 291 292 293 294 295
                _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()],
chirac's avatar
chirac committed
296 297
            'actives_assigned_interfaces': [
                "Interfaces actives et assignées ipv4",
298 299 300 301 302 303 304
                _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()]
chirac's avatar
chirac committed
305
        }],
306
        [["Range d'ip", "Vlan", "Nombre d'ip totales", "Ip assignées",
chirac's avatar
chirac committed
307 308
          "Ip assignées à une machine active", "Ip non assignées"], ip_dict]
        ]
Gabriel Detraz's avatar
Gabriel Detraz committed
309 310 311
    return render(request, 'logs/stats_general.html', {'stats_list': stats})


312
@login_required
313 314 315 316
@can_view_app('users')
@can_view_app('cotisations')
@can_view_app('machines')
@can_view_app('topologie')
317
def stats_models(request):
chirac's avatar
chirac committed
318 319 320
    """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"""
321
    stats = {
chirac's avatar
chirac committed
322 323
        'Users': {
            'users': [User.PRETTY_NAME, User.objects.count()],
324 325
            'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()],
            'clubs': [Club.PRETTY_NAME, Club.objects.count()],
chirac's avatar
chirac committed
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
            'serviceuser': [ServiceUser.PRETTY_NAME,
                            ServiceUser.objects.count()],
            'school': [School.PRETTY_NAME, School.objects.count()],
            'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()],
            'listshell': [ListShell.PRETTY_NAME, ListShell.objects.count()],
            'ban': [Ban.PRETTY_NAME, Ban.objects.count()],
            'whitelist': [Whitelist.PRETTY_NAME, Whitelist.objects.count()]
        },
        'Cotisations': {
            'factures': [Facture.PRETTY_NAME, Facture.objects.count()],
            'vente': [Vente.PRETTY_NAME, Vente.objects.count()],
            'cotisation': [Cotisation.PRETTY_NAME, Cotisation.objects.count()],
            'article': [Article.PRETTY_NAME, Article.objects.count()],
            'banque': [Banque.PRETTY_NAME, Banque.objects.count()],
        },
        'Machines': {
            'machine': [Machine.PRETTY_NAME, Machine.objects.count()],
            'typemachine': [MachineType.PRETTY_NAME,
                            MachineType.objects.count()],
            'typeip': [IpType.PRETTY_NAME, IpType.objects.count()],
            'extension': [Extension.PRETTY_NAME, Extension.objects.count()],
            'interface': [Interface.PRETTY_NAME, Interface.objects.count()],
            'alias': [Domain.PRETTY_NAME,
                      Domain.objects.exclude(cname=None).count()],
            'iplist': [IpList.PRETTY_NAME, IpList.objects.count()],
351 352 353 354 355 356
            'service': [Service.PRETTY_NAME, Service.objects.count()],
            'ouvertureportlist': [
                OuverturePortList.PRETTY_NAME,
                OuverturePortList.objects.count()
            ],
            'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()],
357
            'SOA': [SOA.PRETTY_NAME, SOA.objects.count()],
358 359 360
            'Mx': [Mx.PRETTY_NAME, Mx.objects.count()],
            'Ns': [Ns.PRETTY_NAME, Ns.objects.count()],
            'nas': [Nas.PRETTY_NAME, Nas.objects.count()],
chirac's avatar
chirac committed
361 362 363 364 365
        },
        'Topologie': {
            'switch': [Switch.PRETTY_NAME, Switch.objects.count()],
            'port': [Port.PRETTY_NAME, Port.objects.count()],
            'chambre': [Room.PRETTY_NAME, Room.objects.count()],
366 367 368 369 370 371 372 373 374
            'stack': [Stack.PRETTY_NAME, Stack.objects.count()],
            'modelswitch': [
                ModelSwitch.PRETTY_NAME,
                ModelSwitch.objects.count()
            ],
            'constructorswitch': [
                ConstructorSwitch.PRETTY_NAME,
                ConstructorSwitch.objects.count()
            ],
chirac's avatar
chirac committed
375 376 377 378 379
        },
        'Actions effectuées sur la base':
        {
            'revision': ["Nombre d'actions", Revision.objects.count()],
        },
380
    }
chirac's avatar
chirac committed
381 382
    return render(request, 'logs/stats_models.html', {'stats_list': stats})

chirac's avatar
chirac committed
383 384

@login_required
385
@can_view_app('users')
chirac's avatar
chirac committed
386
def stats_users(request):
chirac's avatar
chirac committed
387 388 389 390
    """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"""
guimoz's avatar
guimoz committed
391 392
    onglet = request.GET.get('onglet')
    try:
chirac's avatar
chirac committed
393 394 395
        _search_field = STATS_DICT[onglet]
    except KeyError:
        _search_field = STATS_DICT[0]
guimoz's avatar
guimoz committed
396
        onglet = 0
chirac's avatar
chirac committed
397
    stats = {
chirac's avatar
chirac committed
398 399 400 401 402 403 404 405 406 407 408 409 410 411
        'Utilisateur': {
            'Machines': User.objects.annotate(
                num=Count('machine')
            ).order_by('-num')[:10],
            'Facture': User.objects.annotate(
                num=Count('facture')
            ).order_by('-num')[:10],
            'Bannissement': User.objects.annotate(
                num=Count('ban')
            ).order_by('-num')[:10],
            'Accès gracieux': User.objects.annotate(
                num=Count('whitelist')
            ).order_by('-num')[:10],
            'Droits': User.objects.annotate(
412
                num=Count('groups')
chirac's avatar
chirac committed
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
            ).order_by('-num')[:10],
        },
        'Etablissement': {
            'Utilisateur': School.objects.annotate(
                num=Count('user')
            ).order_by('-num')[:10],
        },
        'Moyen de paiement': {
            'Utilisateur': Paiement.objects.annotate(
                num=Count('facture')
            ).order_by('-num')[:10],
        },
        'Banque': {
            'Utilisateur': Banque.objects.annotate(
                num=Count('facture')
            ).order_by('-num')[:10],
        },
chirac's avatar
chirac committed
430
    }
chirac's avatar
chirac committed
431 432 433 434 435 436
    return render(request, 'logs/stats_users.html', {
        'stats_list': stats,
        'stats_dict': STATS_DICT,
        'active_field': onglet
        })

chirac's avatar
chirac committed
437 438

@login_required
439
@can_view_app('users')
chirac's avatar
chirac committed
440
def stats_actions(request):
chirac's avatar
chirac committed
441 442 443
    """Vue qui affiche les statistiques de modifications d'objets par
    utilisateurs.
    Affiche le nombre de modifications aggrégées par utilisateurs"""
chirac's avatar
chirac committed
444
    stats = {
chirac's avatar
chirac committed
445 446 447 448 449
        'Utilisateur': {
            'Action': User.objects.annotate(
                num=Count('revision')
            ).order_by('-num')[:40],
        },
chirac's avatar
chirac committed
450 451
    }
    return render(request, 'logs/stats_users.html', {'stats_list': stats})
452 453 454 455

@login_required
@can_view_app('users')
def stats_droits(request):
456 457 458 459 460 461 462 463
    """Affiche la liste des droits et les users ayant chaque droit"""
    depart=time()
    stats_list={}
    
    for droit in ListRight.objects.all().select_related('group_ptr'):#.prefetch_related('group_ptr__user_set__revision_set'):
        stats_list[droit]=droit.user_set.all().annotate(num=Count('revision'),last=Max('revision__date_created'))

    return render(request, 'logs/stats_droits.html', {'stats_list': stats_list})