views.py 18.5 KB
Newer Older
lhark's avatar
lhark committed
1 2 3 4
# 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.
#
5 6 7 8
# Copyright © 2018  Gabriel Détraz
# Copyright © 2018  Goulven Kermarec
# Copyright © 2018  Augustin Lemesle
# Copyright © 2018  Hugo Levy-Falk
lhark's avatar
lhark committed
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#
# 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.

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

from __future__ import unicode_literals
40
from itertools import chain
41

42
from django.urls import reverse
chirac's avatar
chirac committed
43 44
from django.shortcuts import render, redirect
from django.contrib import messages
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
45
from django.contrib.auth.decorators import login_required
46
from django.http import Http404
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
47
from django.db.models import Count
48
from django.apps import apps
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
49
from django.utils.translation import ugettext as _
chirac's avatar
chirac committed
50 51

from reversion.models import Revision
52
from reversion.models import Version, ContentType
chirac's avatar
chirac committed
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
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,
95 96
    ConstructorSwitch,
    AccessPoint
97
)
98
from preferences.models import GeneralOption
chirac's avatar
chirac committed
99
from re2o.views import form
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
100 101 102 103 104
from re2o.utils import (
    all_whitelisted,
    all_baned,
    all_has_access,
    all_adherent,
105
    re2o_paginator,
106 107
)
from re2o.acl import (
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
108 109 110 111
    can_view_all,
    can_view_app,
    can_edit_history,
)
chirac's avatar
Menage  
chirac committed
112
from re2o.utils import all_active_assigned_interfaces_count
113
from re2o.utils import all_active_interfaces_count, SortTable
guimoz's avatar
guimoz committed
114

115

chirac's avatar
chirac committed
116
@login_required
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
117
@can_view_app('logs')
chirac's avatar
chirac committed
118
def index(request):
chirac's avatar
chirac committed
119 120
    """Affiche les logs affinés, date reformatées, selectionne
    les event importants (ajout de droits, ajout de ban/whitelist)"""
121
    pagination_number = GeneralOption.get_cached_value('pagination_number')
122
    # The types of content kept for display
chirac's avatar
chirac committed
123
    content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user']
124
    # Select only wanted versions
chirac's avatar
chirac committed
125 126 127 128
    versions = Version.objects.filter(
        content_type__in=ContentType.objects.filter(
            model__in=content_type_filter
        )
129 130 131 132 133 134 135
    ).select_related('revision')
    versions = SortTable.sort(
        versions,
        request.GET.get('col'),
        request.GET.get('order'),
        SortTable.LOGS_INDEX
    )
136
    versions = re2o_paginator(request, versions, pagination_number)
137 138 139 140 141
    # 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
142 143 144
    for i in range(len(versions.object_list)):
        if versions.object_list[i].object:
            version = versions.object_list[i]
145
            versions.object_list[i] = {
chirac's avatar
chirac committed
146 147 148 149
                'rev_id': version.revision.id,
                'comment': version.revision.comment,
                'datetime': version.revision.date_created.strftime(
                    '%d/%m/%y %H:%M:%S'
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
150
                ),
chirac's avatar
chirac committed
151 152 153 154 155 156 157
                '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)
158
    # Remove all tagged invalid items
chirac's avatar
chirac committed
159
    for i in to_remove:
160 161
        versions.object_list.pop(i)
    return render(request, 'logs/index.html', {'versions_list': versions})
162

chirac's avatar
chirac committed
163

164
@login_required
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
165
@can_view_all(GeneralOption)
166
def stats_logs(request):
chirac's avatar
chirac committed
167 168
    """Affiche l'ensemble des logs et des modifications sur les objets,
    classés par date croissante, en vrac"""
169
    pagination_number = GeneralOption.get_cached_value('pagination_number')
170
    revisions = Revision.objects.all().select_related('user')\
chirac's avatar
chirac committed
171
        .prefetch_related('version_set__object')
172 173 174 175 176 177
    revisions = SortTable.sort(
        revisions,
        request.GET.get('col'),
        request.GET.get('order'),
        SortTable.LOGS_STATS_LOGS
    )
178
    revisions = re2o_paginator(request, revisions, pagination_number)
chirac's avatar
chirac committed
179 180
    return render(request, 'logs/stats_logs.html', {
        'revisions_list': revisions
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
181
    })
chirac's avatar
chirac committed
182

183

184
@login_required
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
185
@can_edit_history
186 187 188 189 190
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
191
        messages.error(request, u"Revision inexistante")
192 193 194
    if request.method == "POST":
        revision.revert()
        messages.success(request, "L'action a été supprimée")
195
        return redirect(reverse('logs:index'))
chirac's avatar
chirac committed
196 197 198
    return form({
        'objet': revision,
        'objet_name': revision.__class__.__name__
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
199
    }, 'logs/delete.html', request)
chirac's avatar
chirac committed
200

201

Gabriel Detraz's avatar
Gabriel Detraz committed
202
@login_required
203
@can_view_all(IpList, Interface, User)
Gabriel Detraz's avatar
Gabriel Detraz committed
204
def stats_general(request):
chirac's avatar
chirac committed
205 206 207 208
    """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()
209
    for ip_range in IpType.objects.select_related('vlan').all():
Gabriel Detraz's avatar
Gabriel Detraz committed
210 211
        all_ip = IpList.objects.filter(ip_type=ip_range)
        used_ip = Interface.objects.filter(ipv4__in=all_ip).count()
chirac's avatar
chirac committed
212 213 214
        active_ip = all_active_assigned_interfaces_count().filter(
            ipv4__in=IpList.objects.filter(ip_type=ip_range)
        ).count()
215
        ip_dict[ip_range] = [ip_range, ip_range.vlan, all_ip.count(),
chirac's avatar
chirac committed
216
                             used_ip, active_ip, all_ip.count()-used_ip]
217 218 219 220 221
    _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()
Maël Kervella's avatar
Maël Kervella committed
222 223
    _all_active_assigned_interfaces_count = \
        all_active_assigned_interfaces_count()
Gabriel Detraz's avatar
Gabriel Detraz committed
224
    stats = [
Maël Kervella's avatar
Maël Kervella committed
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
        [   # First set of data (about users)
            [   # Headers
                "Categorie",
                "Nombre d'utilisateurs (total club et adhérents)",
                "Nombre d'adhérents",
                "Nombre de clubs"
            ],
            {   # Data
                'active_users': [
                    "Users actifs",
                    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': [
                    "Users désactivés",
                    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': [
                    "Users archivés",
                    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': [
                    "Cotisant à l'association",
                    _all_adherent.count(),
                    _all_adherent.exclude(adherent__isnull=True).count(),
                    _all_adherent.exclude(club__isnull=True).count()
                ],
                'connexion_users': [
                    "Utilisateurs bénéficiant d'une connexion",
                    _all_has_access.count(),
                    _all_has_access.exclude(adherent__isnull=True).count(),
                    _all_has_access.exclude(club__isnull=True).count()
                ],
                'ban_users': [
                    "Utilisateurs bannis",
                    _all_baned.count(),
                    _all_baned.exclude(adherent__isnull=True).count(),
                    _all_baned.exclude(club__isnull=True).count()
                ],
                'whitelisted_user': [
                    "Utilisateurs bénéficiant d'une connexion gracieuse",
                    _all_whitelisted.count(),
                    _all_whitelisted.exclude(adherent__isnull=True).count(),
                    _all_whitelisted.exclude(club__isnull=True).count()
                ],
                'actives_interfaces': [
                    "Interfaces actives (ayant accès au reseau)",
                    _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': [
                    "Interfaces actives et assignées 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
                "Range d'ip",
                "Vlan",
                "Nombre d'ip totales",
                "Ip assignées",
                "Ip assignées à une machine active",
                "Ip non assignées"
            ],
            ip_dict  # Data already prepared
chirac's avatar
chirac committed
313
        ]
Maël Kervella's avatar
Maël Kervella committed
314
    ]
Gabriel Detraz's avatar
Gabriel Detraz committed
315 316 317
    return render(request, 'logs/stats_general.html', {'stats_list': stats})


318
@login_required
319
@can_view_app('users', 'cotisations', 'machines', 'topologie')
320
def stats_models(request):
chirac's avatar
chirac committed
321 322 323
    """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"""
324
    stats = {
chirac's avatar
chirac committed
325 326
        'Users': {
            'users': [User.PRETTY_NAME, User.objects.count()],
327 328
            'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()],
            'clubs': [Club.PRETTY_NAME, Club.objects.count()],
chirac's avatar
chirac committed
329 330 331 332 333 334 335 336 337
            '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': {
Maël Kervella's avatar
Maël Kervella committed
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
            'factures': [
                Facture._meta.verbose_name.title(),
                Facture.objects.count()
            ],
            'vente': [
                Vente._meta.verbose_name.title(),
                Vente.objects.count()
            ],
            'cotisation': [
                Cotisation._meta.verbose_name.title(),
                Cotisation.objects.count()
            ],
            'article': [
                Article._meta.verbose_name.title(),
                Article.objects.count()
            ],
            'banque': [
                Banque._meta.verbose_name.title(),
                Banque.objects.count()
            ],
chirac's avatar
chirac committed
358 359 360 361 362 363 364 365 366 367 368
        },
        '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()],
369 370 371 372 373 374
            'service': [Service.PRETTY_NAME, Service.objects.count()],
            'ouvertureportlist': [
                OuverturePortList.PRETTY_NAME,
                OuverturePortList.objects.count()
            ],
            'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()],
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
375
            'SOA': [SOA.PRETTY_NAME, SOA.objects.count()],
376 377 378
            '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
379 380 381
        },
        'Topologie': {
            'switch': [Switch.PRETTY_NAME, Switch.objects.count()],
382
            'bornes': [AccessPoint.PRETTY_NAME, AccessPoint.objects.count()],
chirac's avatar
chirac committed
383 384
            'port': [Port.PRETTY_NAME, Port.objects.count()],
            'chambre': [Room.PRETTY_NAME, Room.objects.count()],
385 386 387 388 389 390 391 392 393
            '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
394 395 396 397 398
        },
        'Actions effectuées sur la base':
        {
            'revision': ["Nombre d'actions", Revision.objects.count()],
        },
399
    }
chirac's avatar
chirac committed
400 401
    return render(request, 'logs/stats_models.html', {'stats_list': stats})

chirac's avatar
chirac committed
402 403

@login_required
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
404
@can_view_app('users')
chirac's avatar
chirac committed
405
def stats_users(request):
chirac's avatar
chirac committed
406 407 408 409
    """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"""
chirac's avatar
chirac committed
410
    stats = {
chirac's avatar
chirac committed
411 412 413 414 415 416 417 418 419 420 421 422 423 424
        '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(
425
                num=Count('groups')
chirac's avatar
chirac committed
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
            ).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
443
    }
Maël Kervella's avatar
Maël Kervella committed
444
    return render(request, 'logs/stats_users.html', {'stats_list': stats})
chirac's avatar
chirac committed
445

chirac's avatar
chirac committed
446 447

@login_required
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
448
@can_view_app('users')
chirac's avatar
chirac committed
449
def stats_actions(request):
chirac's avatar
chirac committed
450 451 452
    """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
453
    stats = {
chirac's avatar
chirac committed
454 455 456 457 458
        'Utilisateur': {
            'Action': User.objects.annotate(
                num=Count('revision')
            ).order_by('-num')[:40],
        },
chirac's avatar
chirac committed
459 460
    }
    return render(request, 'logs/stats_users.html', {'stats_list': stats})
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485


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:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
486
        raise Http404(_("No model found."))
487 488 489 490 491
    object_name_id = object_name + 'id'
    kwargs = {object_name_id: object_id}
    try:
        instance = model.get_instance(**kwargs)
    except model.DoesNotExist:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
492
        messages.error(request, _("No entry found."))
493 494 495 496 497 498
        return redirect(reverse(
            'users:profil',
            kwargs={'userid': str(request.user.id)}
        ))
    can, msg = instance.can_view(request.user)
    if not can:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
499
        messages.error(request, msg or _("You cannot acces to this menu"))
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
        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}
    )