|
|
|
@ -23,62 +23,68 @@ |
|
|
|
# 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 django.http import HttpResponse |
|
|
|
from django.shortcuts import render, redirect |
|
|
|
from django.shortcuts import get_object_or_404 |
|
|
|
from django.template.context_processors import csrf |
|
|
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger |
|
|
|
from django.template import Context, RequestContext, loader |
|
|
|
from django.contrib import messages |
|
|
|
from django.contrib.auth.decorators import login_required, permission_required |
|
|
|
from django.db.models import ProtectedError |
|
|
|
from django.forms import ValidationError |
|
|
|
from django.db import transaction |
|
|
|
from django.db.models import Count |
|
|
|
|
|
|
|
from reversion.models import Revision |
|
|
|
from reversion.models import Version, ContentType |
|
|
|
|
|
|
|
from users.models import User, ServiceUser, Right, School, ListRight, ListShell, Ban, Whitelist |
|
|
|
from users.models import all_has_access, all_whitelisted, all_baned, all_adherent |
|
|
|
from cotisations.models import Facture, Vente, Article, Banque, Paiement, Cotisation |
|
|
|
from machines.models import Machine, MachineType, IpType, Extension, Interface, Domain, IpList |
|
|
|
from machines.views import all_active_assigned_interfaces_count, all_active_interfaces_count |
|
|
|
from users.models import User, ServiceUser, Right, School, ListRight, ListShell |
|
|
|
from users.models import Ban, Whitelist, all_has_access |
|
|
|
from users.models import all_whitelisted, all_baned, all_adherent |
|
|
|
from cotisations.models import Facture, Vente, Article, Banque, Paiement |
|
|
|
from cotisations.models import Cotisation |
|
|
|
from machines.models import Machine, MachineType, IpType, Extension, Interface |
|
|
|
from machines.models import Domain, IpList |
|
|
|
from machines.views import all_active_assigned_interfaces_count |
|
|
|
from machines.views import all_active_interfaces_count |
|
|
|
from topologie.models import Switch, Port, Room |
|
|
|
from preferences.models import GeneralOption |
|
|
|
from re2o.views import form |
|
|
|
|
|
|
|
from django.utils import timezone |
|
|
|
from dateutil.relativedelta import relativedelta |
|
|
|
|
|
|
|
STATS_DICT = { |
|
|
|
0 : ["Tout", 36], |
|
|
|
1 : ["1 mois", 1], |
|
|
|
2 : ["2 mois", 2], |
|
|
|
3 : ["6 mois", 6], |
|
|
|
4 : ["1 an", 12], |
|
|
|
5 : ["2 an", 24], |
|
|
|
0: ["Tout", 36], |
|
|
|
1: ["1 mois", 1], |
|
|
|
2: ["2 mois", 2], |
|
|
|
3: ["6 mois", 6], |
|
|
|
4: ["1 an", 12], |
|
|
|
5: ["2 an", 24], |
|
|
|
} |
|
|
|
|
|
|
|
def form(ctx, template, request): |
|
|
|
c = ctx |
|
|
|
c.update(csrf(request)) |
|
|
|
return render(request, template, c) |
|
|
|
|
|
|
|
@login_required |
|
|
|
@permission_required('cableur') |
|
|
|
def index(request): |
|
|
|
options, created = GeneralOption.objects.get_or_create() |
|
|
|
"""Affiche les logs affinés, date reformatées, selectionne |
|
|
|
les event importants (ajout de droits, ajout de ban/whitelist)""" |
|
|
|
options, _created = GeneralOption.objects.get_or_create() |
|
|
|
pagination_number = options.pagination_number |
|
|
|
|
|
|
|
# The types of content kept for display |
|
|
|
content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user'] |
|
|
|
|
|
|
|
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)).order_by('revision__date_created').reverse().select_related('revision') |
|
|
|
|
|
|
|
versions = Version.objects.filter( |
|
|
|
content_type__in=ContentType.objects.filter( |
|
|
|
model__in=content_type_filter |
|
|
|
) |
|
|
|
).order_by('revision__date_created').reverse().select_related('revision') |
|
|
|
paginator = Paginator(versions, pagination_number) |
|
|
|
page = request.GET.get('page') |
|
|
|
try: |
|
|
|
@ -87,7 +93,7 @@ def index(request): |
|
|
|
# If page is not an integer, deliver first page. |
|
|
|
versions = paginator.page(1) |
|
|
|
except EmptyPage: |
|
|
|
# If page is out of range (e.g. 9999), deliver last page of results. |
|
|
|
# If page is out of range (e.g. 9999), deliver last page of results. |
|
|
|
versions = paginator.page(paginator.num_pages) |
|
|
|
|
|
|
|
# Force to have a list instead of QuerySet |
|
|
|
@ -95,30 +101,38 @@ def index(request): |
|
|
|
# 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 : |
|
|
|
v = versions.object_list[i] |
|
|
|
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' : v.revision.id, |
|
|
|
'comment': v.revision.comment, |
|
|
|
'datetime': v.revision.date_created.strftime('%d/%m/%y %H:%M:%S'), |
|
|
|
'username': v.revision.user.get_username() if v.revision.user else '?', |
|
|
|
'user_id': v.revision.user_id, |
|
|
|
'version': v } |
|
|
|
else : |
|
|
|
to_remove.insert(0,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 : |
|
|
|
for i in to_remove: |
|
|
|
versions.object_list.pop(i) |
|
|
|
|
|
|
|
return render(request, 'logs/index.html', {'versions_list': versions}) |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@permission_required('cableur') |
|
|
|
def stats_logs(request): |
|
|
|
options, created = GeneralOption.objects.get_or_create() |
|
|
|
"""Affiche l'ensemble des logs et des modifications sur les objets, |
|
|
|
classés par date croissante, en vrac""" |
|
|
|
options, _created = GeneralOption.objects.get_or_create() |
|
|
|
pagination_number = options.pagination_number |
|
|
|
revisions = Revision.objects.all().order_by('date_created').reverse().select_related('user').prefetch_related('version_set__object') |
|
|
|
revisions = Revision.objects.all().order_by('date_created')\ |
|
|
|
.reverse().select_related('user')\ |
|
|
|
.prefetch_related('version_set__object') |
|
|
|
paginator = Paginator(revisions, pagination_number) |
|
|
|
page = request.GET.get('page') |
|
|
|
try: |
|
|
|
@ -127,9 +141,12 @@ def stats_logs(request): |
|
|
|
# If page is not an integer, deliver first page. |
|
|
|
revisions = paginator.page(1) |
|
|
|
except EmptyPage: |
|
|
|
# If page is out of range (e.g. 9999), deliver last page of results. |
|
|
|
# If page is out of range (e.g. 9999), deliver last page of results. |
|
|
|
revisions = paginator.page(paginator.num_pages) |
|
|
|
return render(request, 'logs/stats_logs.html', {'revisions_list': revisions}) |
|
|
|
return render(request, 'logs/stats_logs.html', { |
|
|
|
'revisions_list': revisions |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@permission_required('bureau') |
|
|
|
@ -138,121 +155,182 @@ def revert_action(request, revision_id): |
|
|
|
try: |
|
|
|
revision = Revision.objects.get(id=revision_id) |
|
|
|
except Revision.DoesNotExist: |
|
|
|
messages.error(request, u"Revision inexistante" ) |
|
|
|
messages.error(request, u"Revision inexistante") |
|
|
|
if request.method == "POST": |
|
|
|
revision.revert() |
|
|
|
messages.success(request, "L'action a été supprimée") |
|
|
|
return redirect("/logs/") |
|
|
|
return form({'objet': revision, 'objet_name': revision.__class__.__name__ }, 'logs/delete.html', request) |
|
|
|
return form({ |
|
|
|
'objet': revision, |
|
|
|
'objet_name': revision.__class__.__name__ |
|
|
|
}, 'logs/delete.html', request) |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@permission_required('cableur') |
|
|
|
def stats_general(request): |
|
|
|
all_active_users = User.objects.filter(state=User.STATE_ACTIVE) |
|
|
|
ip = dict() |
|
|
|
"""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.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[ip_range] = [ip_range, all_ip.count(), used_ip, active_ip, all_ip.count()-used_ip] |
|
|
|
active_ip = all_active_assigned_interfaces_count().filter( |
|
|
|
ipv4__in=IpList.objects.filter(ip_type=ip_range) |
|
|
|
).count() |
|
|
|
ip_dict[ip_range] = [ip_range, all_ip.count(), |
|
|
|
used_ip, active_ip, all_ip.count()-used_ip] |
|
|
|
stats = [ |
|
|
|
[["Categorie", "Nombre d'utilisateurs"], { |
|
|
|
'active_users' : ["Users actifs", User.objects.filter(state=User.STATE_ACTIVE).count()], |
|
|
|
'inactive_users' : ["Users désactivés", User.objects.filter(state=User.STATE_DISABLED).count()], |
|
|
|
'archive_users' : ["Users archivés", User.objects.filter(state=User.STATE_ARCHIVE).count()], |
|
|
|
'adherent_users' : ["Adhérents à l'association", all_adherent().count()], |
|
|
|
'connexion_users' : ["Utilisateurs bénéficiant d'une connexion", all_has_access().count()], |
|
|
|
'ban_users' : ["Utilisateurs bannis", all_baned().count()], |
|
|
|
'whitelisted_user' : ["Utilisateurs bénéficiant d'une connexion gracieuse", all_whitelisted().count()], |
|
|
|
'actives_interfaces' : ["Interfaces actives (ayant accès au reseau)", all_active_interfaces_count().count()], |
|
|
|
'actives_assigned_interfaces' : ["Interfaces actives et assignées ipv4", all_active_assigned_interfaces_count().count()] |
|
|
|
}], |
|
|
|
[["Range d'ip", "Nombre d'ip totales", "Ip assignées", "Ip assignées à une machine active", "Ip non assignées"] ,ip] |
|
|
|
] |
|
|
|
[["Categorie", "Nombre d'utilisateurs"], { |
|
|
|
'active_users': [ |
|
|
|
"Users actifs", |
|
|
|
User.objects.filter(state=User.STATE_ACTIVE).count()], |
|
|
|
'inactive_users': [ |
|
|
|
"Users désactivés", |
|
|
|
User.objects.filter(state=User.STATE_DISABLED).count()], |
|
|
|
'archive_users': [ |
|
|
|
"Users archivés", |
|
|
|
User.objects.filter(state=User.STATE_ARCHIVE).count()], |
|
|
|
'adherent_users': [ |
|
|
|
"Adhérents à l'association", |
|
|
|
all_adherent().count()], |
|
|
|
'connexion_users': [ |
|
|
|
"Utilisateurs bénéficiant d'une connexion", |
|
|
|
all_has_access().count()], |
|
|
|
'ban_users': [ |
|
|
|
"Utilisateurs bannis", |
|
|
|
all_baned().count()], |
|
|
|
'whitelisted_user': [ |
|
|
|
"Utilisateurs bénéficiant d'une connexion gracieuse", |
|
|
|
all_whitelisted().count()], |
|
|
|
'actives_interfaces': [ |
|
|
|
"Interfaces actives (ayant accès au reseau)", |
|
|
|
all_active_interfaces_count().count()], |
|
|
|
'actives_assigned_interfaces': [ |
|
|
|
"Interfaces actives et assignées ipv4", |
|
|
|
all_active_assigned_interfaces_count().count()] |
|
|
|
}], |
|
|
|
[["Range d'ip", "Nombre d'ip totales", "Ip assignées", |
|
|
|
"Ip assignées à une machine active", "Ip non assignées"], ip_dict] |
|
|
|
] |
|
|
|
return render(request, 'logs/stats_general.html', {'stats_list': stats}) |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@permission_required('cableur') |
|
|
|
def stats_models(request): |
|
|
|
all_active_users = User.objects.filter(state=User.STATE_ACTIVE) |
|
|
|
"""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.PRETTY_NAME, User.objects.count()], |
|
|
|
'serviceuser' : [ServiceUser.PRETTY_NAME, ServiceUser.objects.count()], |
|
|
|
'right' : [Right.PRETTY_NAME, Right.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()], |
|
|
|
'cotisation' : [Cotisation.PRETTY_NAME, Cotisation.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()], |
|
|
|
}, |
|
|
|
'Topologie' : { |
|
|
|
'switch' : [Switch.PRETTY_NAME, Switch.objects.count()], |
|
|
|
'port' : [Port.PRETTY_NAME, Port.objects.count()], |
|
|
|
'chambre' : [Room.PRETTY_NAME, Room.objects.count()], |
|
|
|
}, |
|
|
|
'Actions effectuées sur la base' : |
|
|
|
{ |
|
|
|
'revision' : ["Nombre d'actions", Revision.objects.count()], |
|
|
|
}, |
|
|
|
'Users': { |
|
|
|
'users': [User.PRETTY_NAME, User.objects.count()], |
|
|
|
'serviceuser': [ServiceUser.PRETTY_NAME, |
|
|
|
ServiceUser.objects.count()], |
|
|
|
'right': [Right.PRETTY_NAME, Right.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()], |
|
|
|
}, |
|
|
|
'Topologie': { |
|
|
|
'switch': [Switch.PRETTY_NAME, Switch.objects.count()], |
|
|
|
'port': [Port.PRETTY_NAME, Port.objects.count()], |
|
|
|
'chambre': [Room.PRETTY_NAME, Room.objects.count()], |
|
|
|
}, |
|
|
|
'Actions effectuées sur la base': |
|
|
|
{ |
|
|
|
'revision': ["Nombre d'actions", Revision.objects.count()], |
|
|
|
}, |
|
|
|
} |
|
|
|
return render(request, 'logs/stats_models.html', {'stats_list': stats}) |
|
|
|
return render(request, 'logs/stats_models.html', {'stats_list': stats}) |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@permission_required('cableur') |
|
|
|
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""" |
|
|
|
onglet = request.GET.get('onglet') |
|
|
|
try: |
|
|
|
search_field = STATS_DICT[onglet] |
|
|
|
except: |
|
|
|
search_field = STATS_DICT[0] |
|
|
|
_search_field = STATS_DICT[onglet] |
|
|
|
except KeyError: |
|
|
|
_search_field = STATS_DICT[0] |
|
|
|
onglet = 0 |
|
|
|
start_date = timezone.now() + relativedelta(months=-search_field[1]) |
|
|
|
stats = { |
|
|
|
'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(num=Count('right')).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], |
|
|
|
}, |
|
|
|
'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( |
|
|
|
num=Count('right') |
|
|
|
).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], |
|
|
|
}, |
|
|
|
} |
|
|
|
return render(request, 'logs/stats_users.html', {'stats_list': stats, 'stats_dict' : STATS_DICT, 'active_field': onglet}) |
|
|
|
return render(request, 'logs/stats_users.html', { |
|
|
|
'stats_list': stats, |
|
|
|
'stats_dict': STATS_DICT, |
|
|
|
'active_field': onglet |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@permission_required('cableur') |
|
|
|
def stats_actions(request): |
|
|
|
onglet = request.GET.get('onglet') |
|
|
|
"""Vue qui affiche les statistiques de modifications d'objets par |
|
|
|
utilisateurs. |
|
|
|
Affiche le nombre de modifications aggrégées par utilisateurs""" |
|
|
|
stats = { |
|
|
|
'Utilisateur' : { |
|
|
|
'Action' : User.objects.annotate(num=Count('revision')).order_by('-num')[:40], |
|
|
|
}, |
|
|
|
'Utilisateur': { |
|
|
|
'Action': User.objects.annotate( |
|
|
|
num=Count('revision') |
|
|
|
).order_by('-num')[:40], |
|
|
|
}, |
|
|
|
} |
|
|
|
return render(request, 'logs/stats_users.html', {'stats_list': stats}) |
|
|
|
|