From c791b8e938d2db3d29f3bb1a0a7bb18152d02231 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 10 May 2021 15:40:16 +0200 Subject: [PATCH] api: add sms count statistics (#53856) --- passerelle/api/urls.py | 3 ++- passerelle/api/views.py | 13 +++++++++++- passerelle/plugins.py | 9 +++++++- passerelle/sms/models.py | 35 +++++++++++++++++++++++++++++++ passerelle/sms/views.py | 45 +++++++++++++++++++++++++++++++++++++++- passerelle/urls_utils.py | 12 +++++++++++ tests/test_sms.py | 40 +++++++++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 4 deletions(-) diff --git a/passerelle/api/urls.py b/passerelle/api/urls.py index 0613e97e..2f51c600 100644 --- a/passerelle/api/urls.py +++ b/passerelle/api/urls.py @@ -16,8 +16,9 @@ from django.conf.urls import url -from .views import JobDetailView +from .views import JobDetailView, StatisticsListView urlpatterns = [ url(r'jobs/(?P[\w,-]+)/$', JobDetailView.as_view(), name='api-job'), + url(r'statistics/$', StatisticsListView.as_view(), name='api-statistics-list'), ] diff --git a/passerelle/api/views.py b/passerelle/api/views.py index edf7e073..c09335a5 100644 --- a/passerelle/api/views.py +++ b/passerelle/api/views.py @@ -16,10 +16,11 @@ from django.core.exceptions import PermissionDenied from django.http import Http404, JsonResponse -from django.views.generic import DetailView +from django.views.generic import DetailView, View from passerelle.base.models import Job from passerelle.utils import is_authorized +from passerelle.views import get_all_apps class JobDetailView(DetailView): @@ -45,3 +46,13 @@ class JobDetailView(DetailView): 'done_timestamp': job.done_timestamp, } return JsonResponse({'err': 0, 'data': data}) + + +class StatisticsListView(View): + def get(self, request, *args, **kwargs): + sources = [] + for app in get_all_apps(): + for connector in app.objects.all(): + if hasattr(connector, 'get_statistics_entries'): + sources.extend(connector.get_statistics_entries(request)) + return JsonResponse({'data': sources}) diff --git a/passerelle/plugins.py b/passerelle/plugins.py index 828da858..a1094d19 100644 --- a/passerelle/plugins.py +++ b/passerelle/plugins.py @@ -17,7 +17,7 @@ from django.apps import apps from django.conf.urls import include, url -from .urls_utils import app_enabled, decorated_includes, manager_required, required +from .urls_utils import app_enabled, decorated_includes, manager_required, required, trust_required def register_apps_urls(urlpatterns): @@ -54,5 +54,12 @@ def register_apps_urls(urlpatterns): urls = required(app_enabled(app.label), urls) urls = required(manager_required, urls) after_urls.append(url(url_prefix, include(urls), kwargs={'connector': connector_slug})) + if hasattr(obj, 'get_api_urls'): + url_prefix = '^api/%s/' % connector_slug + urls = obj.get_api_urls() + if urls: + urls = required(app_enabled(app.label), urls) + urls = required(trust_required, urls) + after_urls.append(url(url_prefix, include(urls), kwargs={'connector': connector_slug})) return before_urls + urlpatterns + after_urls diff --git a/passerelle/sms/models.py b/passerelle/sms/models.py index 1cb3489a..989f5185 100644 --- a/passerelle/sms/models.py +++ b/passerelle/sms/models.py @@ -16,8 +16,10 @@ import logging import re +from django.conf.urls import url from django.contrib.postgres.fields import ArrayField from django.db import models +from django.urls import reverse from django.utils import six from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ @@ -99,6 +101,19 @@ class SMSResource(BaseResource): def get_management_urls(cls): return import_string('passerelle.sms.urls.management_urlpatterns') + @classmethod + def get_api_urls(cls): + from .views import SmsStatisticsView + + api_urlpatterns = [ + url( + r'^(?P[\w,-]+)/sms-count/$', + SmsStatisticsView.as_view(), + name='api-statistics-sms-%s' % cls.get_connector_slug(), + ), + ] + return api_urlpatterns + def _get_authorized_display(self): result = [] for key, value in self.AUTHORIZED: @@ -202,6 +217,26 @@ class SMSResource(BaseResource): self.send_msg(**kwargs) SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug) + def get_statistics_entries(self, request): + return [ + { + 'name': _('SMS Count (%s)') % self.slug, + 'url': request.build_absolute_uri( + reverse('api-statistics-sms-%s' % self.get_connector_slug(), kwargs={'slug': self.slug}), + ), + 'id': 'sms_count_%s_%s' % (self.get_connector_slug(), self.slug), + 'filters': [ + { + 'id': 'time_interval', + 'label': _('Interval'), + 'options': [{'id': 'day', 'label': _('Day')}], + 'required': True, + 'default': 'day', + }, + ], + } + ] + class Meta: abstract = True diff --git a/passerelle/sms/views.py b/passerelle/sms/views.py index 7ccf2666..a818a91c 100644 --- a/passerelle/sms/views.py +++ b/passerelle/sms/views.py @@ -1,12 +1,19 @@ +import datetime + from django.apps import apps from django.contrib import messages +from django.db.models import Count +from django.db.models.functions import TruncDay +from django.http import JsonResponse +from django.utils.timezone import make_aware from django.utils.translation import ugettext_lazy as _ -from django.views.generic import FormView +from django.views.generic import FormView, View from passerelle.utils.jsonresponse import APIError from passerelle.views import GenericConnectorMixin from .forms import SmsTestSendForm +from .models import SMSLog class SmsTestSendView(GenericConnectorMixin, FormView): @@ -36,3 +43,39 @@ class SmsTestSendView(GenericConnectorMixin, FormView): else: messages.success(self.request, _('An SMS was just sent')) return super(SmsTestSendView, self).form_valid(form) + + +class SmsStatisticsView(View): + def get(self, request, *args, **kwargs): + if 'time_interval' in request.GET and request.GET['time_interval'] != 'day': + return JsonResponse({'err': 1, 'err_desc': 'unsupported time interval'}) + + logs = SMSLog.objects.filter(appname=kwargs['connector'], slug=kwargs['slug']) + if 'start' in request.GET: + start = make_aware(datetime.datetime.strptime(request.GET['start'], '%Y-%m-%d')) + logs = logs.filter(timestamp__gte=start) + if 'end' in request.GET: + end = make_aware(datetime.datetime.strptime(request.GET['end'], '%Y-%m-%d')) + logs = logs.filter(timestamp__lte=end) + + logs = logs.annotate(day=TruncDay('timestamp')) + logs = logs.values('day').annotate(total=Count('id')).order_by('day') + + x_labels, data = [], [] + for log in logs: + x_labels.append(log['day'].strftime('%Y-%m-%d')) + data.append(log['total']) + + return JsonResponse( + { + 'data': { + 'x_labels': x_labels, + 'series': [ + { + 'label': _('SMS Count'), + 'data': data, + } + ], + } + } + ) diff --git a/passerelle/urls_utils.py b/passerelle/urls_utils.py index 819c9cca..a168ebb1 100644 --- a/passerelle/urls_utils.py +++ b/passerelle/urls_utils.py @@ -15,6 +15,8 @@ except ImportError: from django.http import Http404 from django.views.debug import technical_404_response +from passerelle.utils import is_trusted + class DecoratedURLPattern(URLPattern): def resolve(self, *args, **kwargs): @@ -148,3 +150,13 @@ def manager_required(function=None, login_url=None): if function: return actual_decorator(function) return actual_decorator + + +def trust_required(func): + @wraps(func) + def f(request, *args, **kwargs): + if not (request.user.is_superuser or is_trusted(request)): + raise PermissionDenied() + return func(request, *args, **kwargs) + + return f diff --git a/tests/test_sms.py b/tests/test_sms.py index c0ed21d4..8eb31d9c 100644 --- a/tests/test_sms.py +++ b/tests/test_sms.py @@ -566,3 +566,43 @@ def test_manager(admin_user, app, connector): ResourceLog.objects.filter(levelno=30)[0].extra['exception'] == 'no phone number was authorized: 0033188888888' ) + + +@pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) +def test_api_statistics(app, freezer, connector, admin_user): + resp = app.get('/api/statistics/') + url = [x for x in resp.json['data'] if x['id'] == 'sms_count_ovh_ovhsmsgateway'][0]['url'] + + assert app.get(url, status=403) + + login(app) + resp = app.get(url) + assert len(resp.json['data']['series'][0]['data']) == 0 + + freezer.move_to('2021-01-01 12:00') + for _ in range(5): + SMSLog.objects.create(appname='ovh', slug='ovhsmsgateway') + + freezer.move_to('2021-02-03 13:00') + for _ in range(3): + SMSLog.objects.create(appname='ovh', slug='ovhsmsgateway') + + freezer.move_to('2021-02-06 13:00') + SMSLog.objects.create(appname='ovh', slug='ovhsmsgateway') + SMSLog.objects.create(appname='ovh', slug='other') + + resp = app.get(url + '?time_interval=day') + assert resp.json['data'] == { + 'x_labels': ['2021-01-01', '2021-02-03', '2021-02-06'], + 'series': [{'label': 'SMS Count', 'data': [5, 3, 1]}], + } + + resp = app.get(url + '?start=2021-02-04&end=2021-02-07') + assert resp.json['data'] == { + 'x_labels': ['2021-02-06'], + 'series': [{'label': 'SMS Count', 'data': [1]}], + } + + # invalid time_interval + resp = app.get(url + '?time_interval=month') + assert resp.json['err'] == 1 -- 2.20.1