Ce serveur Gitlab sera éteint le 30 juin 2020, pensez à migrer vos projets vers les serveurs gitlab-research.centralesupelec.fr et gitlab-student.centralesupelec.fr !

views.py 18.9 KB
Newer Older
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
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.

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
43 44
from django.shortcuts import render, redirect
from django.contrib import messages
45
from django.contrib.auth.decorators import login_required
46
from django.http import Http404
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
47
from django.db.models import Count
48
from django.apps import apps
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
49
from django.utils.translation import ugettext as _
50 51

from reversion.models import Revision
52
from reversion.models import Version, ContentType
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
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 (
108 109 110 111
    can_view_all,
    can_view_app,
    can_edit_history,
)
chirac's avatar
chirac committed
112
from re2o.utils import all_active_assigned_interfaces_count
113
from re2o.utils import all_active_interfaces_count, SortTable
114

115

116
@login_required
117
@can_view_app('logs')
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'
Levy--Falk Hugo's avatar
Levy--Falk Hugo 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
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
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
181
    })
chirac's avatar
chirac committed
182

183

184
@login_required
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:
191
        messages.error(request, _("Nonexistent revision."))
192 193
    if request.method == "POST":
        revision.revert()
194
        messages.success(request, _("The action was deleted."))
195
        return redirect(reverse('logs:index'))
chirac's avatar
chirac committed
196 197 198
    return form({
        'objet': revision,
        'objet_name': revision.__class__.__name__
Levy--Falk Hugo's avatar
Levy--Falk Hugo 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
        [   # First set of data (about users)
            [   # Headers
227 228 229 230
                _("Category"),
                _("Number of users (members and clubs)"),
                _("Number of members"),
                _("Number of clubs")
Maël Kervella's avatar
Maël Kervella committed
231 232 233
            ],
            {   # Data
                'active_users': [
234
                    _("Activated users"),
Maël Kervella's avatar
Maël Kervella committed
235 236 237 238 239 240 241
                    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': [
242
                    _("Disabled users"),
Maël Kervella's avatar
Maël Kervella committed
243 244 245 246 247 248 249
                    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': [
250
                    _("Archived users"),
Maël Kervella's avatar
Maël Kervella committed
251 252 253 254 255 256 257
                    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': [
258
                    _("Contributing members"),
Maël Kervella's avatar
Maël Kervella committed
259 260 261 262 263
                    _all_adherent.count(),
                    _all_adherent.exclude(adherent__isnull=True).count(),
                    _all_adherent.exclude(club__isnull=True).count()
                ],
                'connexion_users': [
264
                    _("Users benefiting from a connection"),
Maël Kervella's avatar
Maël Kervella committed
265 266 267 268 269
                    _all_has_access.count(),
                    _all_has_access.exclude(adherent__isnull=True).count(),
                    _all_has_access.exclude(club__isnull=True).count()
                ],
                'ban_users': [
270
                    _("Banned users"),
Maël Kervella's avatar
Maël Kervella committed
271 272 273 274 275
                    _all_baned.count(),
                    _all_baned.exclude(adherent__isnull=True).count(),
                    _all_baned.exclude(club__isnull=True).count()
                ],
                'whitelisted_user': [
276
                    _("Users benefiting from a free connection"),
Maël Kervella's avatar
Maël Kervella committed
277 278 279 280 281
                    _all_whitelisted.count(),
                    _all_whitelisted.exclude(adherent__isnull=True).count(),
                    _all_whitelisted.exclude(club__isnull=True).count()
                ],
                'actives_interfaces': [
282
                    _("Active interfaces (with access to the network)"),
Maël Kervella's avatar
Maël Kervella committed
283 284 285 286 287 288 289 290 291
                    _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': [
292
                    _("Active interfaces assigned IPv4"),
Maël Kervella's avatar
Maël Kervella committed
293 294 295 296 297 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())
                ]
            }
        ],
        [   # Second set of data (about ip adresses)
            [   # Headers
305 306 307 308 309 310
                _("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")
Maël Kervella's avatar
Maël Kervella committed
311 312
            ],
            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 = {
325 326 327 328 329
        _("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,
chirac's avatar
chirac committed
330
                            ServiceUser.objects.count()],
331 332 333 334 335
            '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()]
chirac's avatar
chirac committed
336
        },
337
        _("Subscriptions"): {
Maël Kervella's avatar
Maël Kervella committed
338
            'factures': [
339
                Facture._meta.verbose_name,
Maël Kervella's avatar
Maël Kervella committed
340 341 342
                Facture.objects.count()
            ],
            'vente': [
343
                Vente._meta.verbose_name,
Maël Kervella's avatar
Maël Kervella committed
344 345 346
                Vente.objects.count()
            ],
            'cotisation': [
347
                Cotisation._meta.verbose_name,
Maël Kervella's avatar
Maël Kervella committed
348 349 350
                Cotisation.objects.count()
            ],
            'article': [
351
                Article._meta.verbose_name,
Maël Kervella's avatar
Maël Kervella committed
352 353 354
                Article.objects.count()
            ],
            'banque': [
355
                Banque._meta.verbose_name,
Maël Kervella's avatar
Maël Kervella committed
356 357
                Banque.objects.count()
            ],
chirac's avatar
chirac committed
358
        },
359 360 361 362
        _("Machines"): {
            'machine': [Machine._meta.verbose_name,
                        Machine.objects.count()],
            'typemachine': [MachineType._meta.verbose_name,
chirac's avatar
chirac committed
363
                            MachineType.objects.count()],
364 365 366 367 368 369 370
            '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,
chirac's avatar
chirac committed
371
                      Domain.objects.exclude(cname=None).count()],
372 373 374 375
            'iplist': [IpList._meta.verbose_name,
                       IpList.objects.count()],
            'service': [Service._meta.verbose_name,
                        Service.objects.count()],
376
            'ouvertureportlist': [
377
                OuverturePortList._meta.verbose_name,
378 379
                OuverturePortList.objects.count()
            ],
380 381 382 383 384
            '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()],
chirac's avatar
chirac committed
385
        },
386 387 388 389 390 391 392 393
        _("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()],
394
            'modelswitch': [
395
                ModelSwitch._meta.verbose_name,
396 397 398
                ModelSwitch.objects.count()
            ],
            'constructorswitch': [
399
                ConstructorSwitch._meta.verbose_name,
400 401
                ConstructorSwitch.objects.count()
            ],
chirac's avatar
chirac committed
402
        },
403
        _("Actions performed"):
chirac's avatar
chirac committed
404
        {
405
            'revision': [_("Number of actions"), Revision.objects.count()],
chirac's avatar
chirac committed
406
        },
407
    }
chirac's avatar
chirac committed
408 409
    return render(request, 'logs/stats_models.html', {'stats_list': stats})

chirac's avatar
chirac committed
410 411

@login_required
412
@can_view_app('users')
chirac's avatar
chirac committed
413
def stats_users(request):
chirac's avatar
chirac committed
414 415 416 417
    """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
418
    stats = {
419 420
        _("User"): {
            _("Machines"): User.objects.annotate(
chirac's avatar
chirac committed
421 422
                num=Count('machine')
            ).order_by('-num')[:10],
423
            _("Invoice"): User.objects.annotate(
chirac's avatar
chirac committed
424 425
                num=Count('facture')
            ).order_by('-num')[:10],
426
            _("Ban"): User.objects.annotate(
chirac's avatar
chirac committed
427 428
                num=Count('ban')
            ).order_by('-num')[:10],
429
            _("Whitelist"): User.objects.annotate(
chirac's avatar
chirac committed
430 431
                num=Count('whitelist')
            ).order_by('-num')[:10],
432
            _("Rights"): User.objects.annotate(
433
                num=Count('groups')
chirac's avatar
chirac committed
434 435
            ).order_by('-num')[:10],
        },
436 437
        _("School"): {
            _("User"): School.objects.annotate(
chirac's avatar
chirac committed
438 439 440
                num=Count('user')
            ).order_by('-num')[:10],
        },
441 442
        _("Payment method"): {
            _("User"): Paiement.objects.annotate(
chirac's avatar
chirac committed
443 444 445
                num=Count('facture')
            ).order_by('-num')[:10],
        },
446 447
        _("Bank"): {
            _("User"): Banque.objects.annotate(
chirac's avatar
chirac committed
448 449 450
                num=Count('facture')
            ).order_by('-num')[:10],
        },
chirac's avatar
chirac committed
451
    }
452
    return render(request, 'logs/stats_users.html', {'stats_list': stats})
chirac's avatar
chirac committed
453

chirac's avatar
chirac committed
454 455

@login_required
456
@can_view_app('users')
chirac's avatar
chirac committed
457
def stats_actions(request):
chirac's avatar
chirac committed
458 459 460
    """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
461
    stats = {
462 463
        _("User"): {
            _("Action"): User.objects.annotate(
chirac's avatar
chirac committed
464 465 466
                num=Count('revision')
            ).order_by('-num')[:40],
        },
chirac's avatar
chirac committed
467 468
    }
    return render(request, 'logs/stats_users.html', {'stats_list': stats})
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493


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:
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
494
        raise Http404(_("No model found."))
495 496 497 498 499
    object_name_id = object_name + 'id'
    kwargs = {object_name_id: object_id}
    try:
        instance = model.get_instance(**kwargs)
    except model.DoesNotExist:
500
        messages.error(request, _("Nonexistent entry."))
501 502 503 504 505 506
        return redirect(reverse(
            'users:profil',
            kwargs={'userid': str(request.user.id)}
        ))
    can, msg = instance.can_view(request.user)
    if not can:
507
        messages.error(request, msg or _("You don't have the right to access this menu."))
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
        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}
    )
524