api: add statistics endpoints (#48845)
This commit is contained in:
parent
206fec2122
commit
2cc198dd70
|
@ -29,6 +29,7 @@ from django.utils.translation import ugettext as _
|
|||
from django.utils.text import slugify
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
@ -40,7 +41,7 @@ from requests.exceptions import RequestException
|
|||
from rest_framework import serializers, pagination
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.viewsets import ModelViewSet, ViewSet
|
||||
from rest_framework.routers import SimpleRouter
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
|
@ -64,6 +65,7 @@ from . import (utils, decorators, attribute_kinds, app_settings, hooks,
|
|||
api_mixins)
|
||||
from .models import Attribute, PasswordReset, Service
|
||||
from .a2_rbac.utils import get_default_ou
|
||||
from .journal_event_types import UserLogin, UserRegistration
|
||||
|
||||
|
||||
# Retro-compatibility with older Django versions
|
||||
|
@ -1097,3 +1099,123 @@ class AddressAutocompleteAPI(APIView):
|
|||
|
||||
|
||||
address_autocomplete = AddressAutocompleteAPI.as_view()
|
||||
|
||||
|
||||
class ServiceOUField(serializers.ListField):
|
||||
def to_internal_value(self, data_list):
|
||||
data = data_list[0].split(' ')
|
||||
if not len(data) == 2:
|
||||
raise ValidationError('This field should be a service slug and an OU slug separated by space.')
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class StatisticsSerializer(serializers.Serializer):
|
||||
time_interval = serializers.ChoiceField(choices=['timestamp', 'day', 'month', 'year'])
|
||||
service = ServiceOUField(child=serializers.SlugField(max_length=256), required=False)
|
||||
ou = serializers.SlugField(required=False, allow_blank=False, max_length=256)
|
||||
start = serializers.DateTimeField(required=False)
|
||||
end = serializers.DateTimeField(required=False)
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('service') and data.get('ou'):
|
||||
raise ValidationError('Organizational Unit and Service must not be given at the same time.')
|
||||
return data
|
||||
|
||||
|
||||
def stat(**kwargs):
|
||||
'''Extend action decorator to allow passing statistics related info.'''
|
||||
filters = kwargs.pop('filters', [])
|
||||
decorator = action(**kwargs)
|
||||
|
||||
def wraps(func):
|
||||
func.filters = filters
|
||||
return decorator(func)
|
||||
return wraps
|
||||
|
||||
|
||||
class StatisticsAPI(ViewSet):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def list(self, request):
|
||||
statistics = []
|
||||
ous = [{'id': ou.slug, 'label': ou.name} for ou in get_ou_model().objects.all()]
|
||||
services = [
|
||||
{'id': '%s %s' % (service['slug'], service['ou__slug']), 'label': service['name']}
|
||||
for service in Service.objects.values('slug', 'name', 'ou__slug')
|
||||
]
|
||||
|
||||
for action in self.get_extra_actions():
|
||||
url = self.reverse_action(action.url_name)
|
||||
data = {
|
||||
'name': action.kwargs['name'],
|
||||
'url': url,
|
||||
'id': action.url_name,
|
||||
'filters': [],
|
||||
}
|
||||
if 'ou' in action.filters:
|
||||
data['filters'].append({'id': 'ou', 'label': _('Organizational Unit'), 'options': ous})
|
||||
if 'service' in action.filters:
|
||||
data['filters'].append({'id': 'service', 'label': _('Service'), 'options': services})
|
||||
statistics.append(data)
|
||||
|
||||
return Response({
|
||||
'data': statistics,
|
||||
'err': 0,
|
||||
})
|
||||
|
||||
def get_statistics(self, request, klass, method):
|
||||
serializer = StatisticsSerializer(data=request.query_params)
|
||||
if not serializer.is_valid():
|
||||
response = {
|
||||
'data': [],
|
||||
'err': 1,
|
||||
'err_desc': serializer.errors
|
||||
}
|
||||
return Response(response, status.HTTP_400_BAD_REQUEST)
|
||||
data = serializer.validated_data
|
||||
|
||||
kwargs = {
|
||||
'group_by_time': data['time_interval'],
|
||||
'start': data.get('start'),
|
||||
'end': data.get('end'),
|
||||
}
|
||||
|
||||
allowed_filters = getattr(self, self.action).filters
|
||||
service, ou = data.get('service'), data.get('ou')
|
||||
if service and 'service' in allowed_filters:
|
||||
service_slug, ou_slug = service
|
||||
kwargs['service'] = get_object_or_404(Service, slug=service_slug, ou__slug=ou_slug)
|
||||
elif ou and 'ou' in allowed_filters:
|
||||
kwargs['ou'] = get_object_or_404(get_ou_model(), slug=ou)
|
||||
|
||||
return Response({
|
||||
'data': getattr(klass, method)(**kwargs),
|
||||
'err': 0,
|
||||
})
|
||||
|
||||
@stat(detail=False, name=_('Login count by authentication type'), filters=('ou', 'service'))
|
||||
def login(self, request):
|
||||
return self.get_statistics(request, UserLogin, 'get_method_statistics')
|
||||
|
||||
@stat(detail=False, name=_('Login count by service'))
|
||||
def service_login(self, request):
|
||||
return self.get_statistics(request, UserLogin, 'get_service_statistics')
|
||||
|
||||
@stat(detail=False, name=_('Login count by organizational unit'))
|
||||
def service_ou_login(self, request):
|
||||
return self.get_statistics(request, UserLogin, 'get_service_ou_statistics')
|
||||
|
||||
@stat(detail=False, name=_('Registration count by type'), filters=('ou', 'service'))
|
||||
def registration(self, request):
|
||||
return self.get_statistics(request, UserRegistration, 'get_method_statistics')
|
||||
|
||||
@stat(detail=False, name=_('Registration count by service'))
|
||||
def service_registration(self, request):
|
||||
return self.get_statistics(request, UserRegistration, 'get_service_statistics')
|
||||
|
||||
@stat(detail=False, name=_('Registration count by organizational unit'))
|
||||
def service_ou_registration(self, request):
|
||||
return self.get_statistics(request, UserRegistration, 'get_service_ou_statistics')
|
||||
|
||||
|
||||
router.register(r'statistics', StatisticsAPI, base_name='a2-api-statistics')
|
||||
|
|
|
@ -35,12 +35,14 @@ from django.utils.text import slugify
|
|||
from django.utils.timezone import now
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from rest_framework import VERSION as drf_version
|
||||
from django_rbac.models import SEARCH_OP
|
||||
from django_rbac.utils import get_role_model, get_ou_model
|
||||
from requests.models import Response
|
||||
|
||||
from authentic2.a2_rbac.models import Role
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2.apps.journal.models import EventType, Event
|
||||
from authentic2.models import Service, Attribute, AttributeValue, AuthorizedRole
|
||||
from authentic2.utils import good_next_url
|
||||
|
||||
|
@ -2054,3 +2056,132 @@ def test_api_password_change_mark_as_deleted(app, settings, admin, ou1):
|
|||
user2.mark_as_deleted()
|
||||
response = app.post_json(url, params=payload)
|
||||
assert User.objects.get(username='john.doe').check_password('password2')
|
||||
|
||||
|
||||
@pytest.mark.skipif(drf_version.startswith('3.4'), reason='no support for old django rest framework')
|
||||
def test_api_statistics_list(app, admin):
|
||||
headers = basic_authorization_header(admin)
|
||||
resp = app.get('/api/statistics/', headers=headers)
|
||||
assert len(resp.json['data']) == 6
|
||||
login_stats = {
|
||||
'name': 'Login count by authentication type',
|
||||
'url': 'http://testserver/api/statistics/login/',
|
||||
'id': 'login',
|
||||
'filters': [
|
||||
{
|
||||
'id': 'ou',
|
||||
'label': 'Organizational Unit',
|
||||
'options': [{'id': 'default', 'label': 'Default organizational unit'}],
|
||||
},
|
||||
{'id': 'service', 'label': 'Service', 'options': []},
|
||||
],
|
||||
}
|
||||
assert login_stats in resp.json['data']
|
||||
assert {
|
||||
'name': 'Login count by service',
|
||||
'url': 'http://testserver/api/statistics/service_login/',
|
||||
'id': 'service-login',
|
||||
'filters': [],
|
||||
} in resp.json['data']
|
||||
|
||||
service = Service.objects.create(name='Service1', slug='service1', ou=get_default_ou())
|
||||
service = Service.objects.create(name='Service2', slug='service2', ou=get_default_ou())
|
||||
login_stats['filters'][1]['options'].append({'id': 'service1 default', 'label': 'Service1'})
|
||||
login_stats['filters'][1]['options'].append({'id': 'service2 default', 'label': 'Service2'})
|
||||
|
||||
resp = app.get('/api/statistics/', headers=headers)
|
||||
assert login_stats in resp.json['data']
|
||||
|
||||
|
||||
@pytest.mark.skipif(drf_version.startswith('3.4'), reason='no support for old django rest framework')
|
||||
@pytest.mark.parametrize(
|
||||
'event_type_name,event_name', [('user.login', 'login'), ('user.registration', 'registration')]
|
||||
)
|
||||
def test_api_statistics(app, admin, freezer, event_type_name, event_name):
|
||||
headers = basic_authorization_header(admin)
|
||||
resp = app.get('/api/statistics/login/', headers=headers, status=400)
|
||||
assert resp.json == {"data": [], "err": 1, "err_desc": {"time_interval": ["This field is required."]}}
|
||||
|
||||
resp = app.get('/api/statistics/login/?time_interval=month', headers=headers)
|
||||
assert resp.json == {"data": {"series": [], "x_labels": []}, "err": 0}
|
||||
|
||||
user = User.objects.create(username='john.doe', email='john.doe@example.com')
|
||||
portal = Service.objects.create(name='portal', slug='portal', ou=get_default_ou())
|
||||
agendas = Service.objects.create(name='agendas', slug='agendas', ou=get_default_ou())
|
||||
|
||||
method = {'how': 'password-on-https'}
|
||||
method2 = {'how': 'fc'}
|
||||
|
||||
event_type = EventType.objects.get_for_name(event_type_name)
|
||||
event_type_definition = event_type.definition
|
||||
|
||||
freezer.move_to('2020-02-03 12:00')
|
||||
event = Event.objects.create(type=event_type, references=[portal], data=method)
|
||||
event = Event.objects.create(type=event_type, references=[agendas], data=method)
|
||||
|
||||
freezer.move_to('2020-03-04 13:00')
|
||||
event = Event.objects.create(type=event_type, references=[agendas], data=method)
|
||||
event = Event.objects.create(type=event_type, references=[portal], data=method2)
|
||||
|
||||
resp = app.get('/api/statistics/%s/?time_interval=month' % event_name, headers=headers)
|
||||
data = resp.json['data']
|
||||
data['series'].sort(key=lambda x: x['label'])
|
||||
assert data == {
|
||||
'x_labels': ['2020-02', '2020-03'],
|
||||
'series': [{'label': 'FranceConnect', 'data': [None, 1]}, {'label': 'password', 'data': [2, 1]}],
|
||||
}
|
||||
|
||||
resp = app.get('/api/statistics/%s/?time_interval=month&ou=default' % event_name, headers=headers)
|
||||
data = resp.json['data']
|
||||
data['series'].sort(key=lambda x: x['label'])
|
||||
assert data == {
|
||||
'x_labels': ['2020-02', '2020-03'],
|
||||
'series': [{'label': 'FranceConnect', 'data': [None, 1]}, {'label': 'password', 'data': [2, 1]}],
|
||||
}
|
||||
|
||||
resp = app.get(
|
||||
'/api/statistics/%s/?time_interval=month&service=agendas default' % event_name, headers=headers
|
||||
)
|
||||
data = resp.json['data']
|
||||
assert data == {'x_labels': ['2020-02', '2020-03'], 'series': [{'label': 'password', 'data': [1, 1]}]}
|
||||
|
||||
resp = app.get(
|
||||
'/api/statistics/%s/?time_interval=month&start=2020-03-01T01:01' % event_name, headers=headers
|
||||
)
|
||||
data = resp.json['data']
|
||||
data['series'].sort(key=lambda x: x['label'])
|
||||
assert data == {
|
||||
'x_labels': ['2020-03'],
|
||||
'series': [{'label': 'FranceConnect', 'data': [1]}, {'label': 'password', 'data': [1]}],
|
||||
}
|
||||
|
||||
resp = app.get(
|
||||
'/api/statistics/%s/?time_interval=month&end=2020-03-01T01:01' % event_name, headers=headers
|
||||
)
|
||||
data = resp.json['data']
|
||||
assert data == {'x_labels': ['2020-02'], 'series': [{'label': 'password', 'data': [2]}]}
|
||||
|
||||
resp = app.get(
|
||||
'/api/statistics/%s/?time_interval=year&service=portal default' % event_name, headers=headers
|
||||
)
|
||||
data = resp.json['data']
|
||||
data['series'].sort(key=lambda x: x['label'])
|
||||
assert data == {
|
||||
'x_labels': ['2020'],
|
||||
'series': [{'label': 'FranceConnect', 'data': [1]}, {'label': 'password', 'data': [1]}],
|
||||
}
|
||||
|
||||
resp = app.get('/api/statistics/service_%s/?time_interval=month' % event_name, headers=headers)
|
||||
data = resp.json['data']
|
||||
data['series'].sort(key=lambda x: x['label'])
|
||||
assert data == {
|
||||
'x_labels': ['2020-02', '2020-03'],
|
||||
'series': [{'label': 'agendas', 'data': [1, 1]}, {'label': 'portal', 'data': [1, 1]}],
|
||||
}
|
||||
|
||||
resp = app.get('/api/statistics/service_ou_%s/?time_interval=month' % event_name, headers=headers)
|
||||
data = resp.json['data']
|
||||
assert data == {
|
||||
'x_labels': ['2020-02', '2020-03'],
|
||||
'series': [{'label': 'Default organizational unit', 'data': [2, 2]}],
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue