diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index 48c393c26..735941070 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -1,4 +1,4 @@ -from sqlalchemy import (desc, func, Integer, and_, asc) +from sqlalchemy import (desc, func, Integer, and_, or_, asc) from sqlalchemy.sql.expression import cast from datetime import ( @@ -12,6 +12,7 @@ from werkzeug.datastructures import MultiDict from app import db from app.models import ( + Service, Notification, Job, NotificationStatistics, @@ -51,6 +52,57 @@ def dao_get_notification_statistics_for_service_and_day(service_id, day): ).order_by(desc(NotificationStatistics.day)).first() +def dao_get_notification_statistics_for_day(day): + return NotificationStatistics.query.filter_by( + day=day + ).all() + + +def dao_get_potential_notification_statistics_for_day(day): + all_services = db.session.query( + Service.id, + NotificationStatistics + ).outerjoin( + Service.service_notification_stats + ).filter( + or_( + NotificationStatistics.day == day, + NotificationStatistics.day == None # noqa + ) + ).order_by( + asc(Service.created_at) + ) + + notification_statistics = [] + for service_notification_stats_pair in all_services: + if service_notification_stats_pair.NotificationStatistics: + notification_statistics.append( + service_notification_stats_pair.NotificationStatistics + ) + else: + notification_statistics.append( + create_notification_statistics_dict( + service_notification_stats_pair, + day + ) + ) + return notification_statistics + + +def create_notification_statistics_dict(service_id, day): + return { + 'id': None, + 'emails_requested': 0, + 'emails_delivered': 0, + 'emails_failed': 0, + 'sms_requested': 0, + 'sms_delivered': 0, + 'sms_failed': 0, + 'day': day.isoformat(), + 'service': service_id + } + + def dao_get_7_day_agg_notification_statistics_for_service(service_id, date_from, week_count=52): diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 8c5e509ae..679a3cdfc 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, date import statsd import itertools from flask import ( @@ -27,7 +27,9 @@ from app.schemas import ( email_notification_schema, sms_template_notification_schema, notification_status_schema, - notifications_filter_schema + notifications_filter_schema, + notifications_statistics_schema, + day_schema, ) from app.celery.tasks import send_sms, send_email @@ -384,3 +386,17 @@ def send_notification(notification_type): statsd_client.incr('notifications.api.{}'.format(notification_type)) return jsonify(data={"notification": {"id": notification_id}}), 201 + + +@notifications.route('/notifications/statistics') +def get_notification_statistics_for_day(): + data, errors = day_schema.load(request.args) + if errors: + return jsonify(result='error', message=errors), 400 + + statistics = notifications_dao.dao_get_potential_notification_statistics_for_day( + day=data['day'] + ) + + data, errors = notifications_statistics_schema.dump(statistics, many=True) + return jsonify(data=data), 200 diff --git a/app/schemas.py b/app/schemas.py index 64f4bbf98..35ca0006a 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -168,7 +168,6 @@ class RequestVerifyCodeSchema(ma.Schema): class NotificationSchema(ma.Schema): personalisation = fields.Dict(required=False) - pass class SmsNotificationSchema(NotificationSchema): @@ -360,6 +359,14 @@ class FromToDateSchema(ma.Schema): raise ValidationError("date_from needs to be greater than date_to") +class DaySchema(ma.Schema): + day = fields.Date(required=True) + + @validates('day') + def validate_day(self, value): + _validate_not_in_future(value) + + class WeekAggregateNotificationStatisticsSchema(ma.Schema): date_from = fields.Date() @@ -405,3 +412,4 @@ event_schema = EventSchema() from_to_date_schema = FromToDateSchema() provider_details_schema = ProviderDetailsSchema() week_aggregate_notification_statistics_schema = WeekAggregateNotificationStatisticsSchema() +day_schema = DaySchema() diff --git a/tests/app/notifications/rest/__init__.py b/tests/app/notifications/rest/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/app/notifications/rest/test_notification_statistics.py b/tests/app/notifications/rest/test_notification_statistics.py new file mode 100644 index 000000000..ec7a0ba0f --- /dev/null +++ b/tests/app/notifications/rest/test_notification_statistics.py @@ -0,0 +1,196 @@ +from datetime import date, timedelta + +from flask import json +from freezegun import freeze_time + +from tests import create_authorization_header +from tests.app.conftest import ( + sample_notification_statistics as create_sample_notification_statistics, + sample_service as create_sample_service +) + + +def test_get_notification_statistics(notify_api, sample_notification_statistics): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_notification_statistics.service_id + ) + + response = client.get( + '/notifications/statistics?day={}'.format(date.today().isoformat()), + headers=[auth_header] + ) + + notifications = json.loads(response.get_data(as_text=True)) + + assert len(notifications['data']) == 1 + stats = notifications['data'][0] + assert stats['emails_requested'] == 2 + assert stats['emails_delivered'] == 1 + assert stats['emails_failed'] == 1 + assert stats['sms_requested'] == 2 + assert stats['sms_delivered'] == 1 + assert stats['sms_failed'] == 1 + assert stats['service'] == str(sample_notification_statistics.service_id) + assert response.status_code == 200 + + +@freeze_time('1955-11-05T12:00:00') +def test_get_notification_statistics_only_returns_today(notify_api, notify_db, notify_db_session, sample_service): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + yesterdays_notification_statistics = create_sample_notification_statistics( + notify_db, + notify_db_session, + service=sample_service, + day=date.today() - timedelta(days=1) + ) + todays_notification_statistics = create_sample_notification_statistics( + notify_db, + notify_db_session, + service=sample_service, + day=date.today() + ) + tomorrows_notification_statistics = create_sample_notification_statistics( + notify_db, + notify_db_session, + service=sample_service, + day=date.today() + timedelta(days=1) + ) + + auth_header = create_authorization_header( + service_id=sample_service.id + ) + + response = client.get( + '/notifications/statistics?day={}'.format(date.today().isoformat()), + headers=[auth_header] + ) + + notifications = json.loads(response.get_data(as_text=True)) + + assert len(notifications['data']) == 1 + assert notifications['data'][0]['day'] == date.today().isoformat() + assert response.status_code == 200 + + +def test_get_notification_statistics_fails_if_no_date(notify_api, sample_notification_statistics): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_notification_statistics.service_id + ) + + response = client.get( + '/notifications/statistics', + headers=[auth_header] + ) + + resp = json.loads(response.get_data(as_text=True)) + assert resp['result'] == 'error' + assert resp['message'] == {'day': ['Missing data for required field.']} + assert response.status_code == 400 + + +def test_get_notification_statistics_fails_if_invalid_date(notify_api, sample_notification_statistics): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_notification_statistics.service_id + ) + + response = client.get( + '/notifications/statistics?day=2016-99-99', + headers=[auth_header] + ) + + resp = json.loads(response.get_data(as_text=True)) + assert resp['result'] == 'error' + assert resp['message'] == {'day': ['Not a valid date.']} + assert response.status_code == 400 + + +def test_get_notification_statistics_returns_zeros_if_not_in_db(notify_api, sample_service): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_service.id + ) + + response = client.get( + '/notifications/statistics?day={}'.format(date.today().isoformat()), + headers=[auth_header] + ) + + notifications = json.loads(response.get_data(as_text=True)) + + assert len(notifications['data']) == 1 + stats = notifications['data'][0] + assert stats['emails_requested'] == 0 + assert stats['emails_delivered'] == 0 + assert stats['emails_failed'] == 0 + assert stats['sms_requested'] == 0 + assert stats['sms_delivered'] == 0 + assert stats['sms_failed'] == 0 + assert stats['service'] == str(sample_service.id) + assert response.status_code == 200 + + +def test_get_notification_statistics_returns_both_existing_stats_and_generated_zeros( + notify_api, + notify_db, + notify_db_session +): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + service_with_stats = create_sample_service( + notify_db, + notify_db_session, + service_name='service_with_stats', + email_from='service_with_stats' + ) + service_without_stats = create_sample_service( + notify_db, + notify_db_session, + service_name='service_without_stats', + email_from='service_without_stats' + ) + notification_statistics = create_sample_notification_statistics( + notify_db, + notify_db_session, + service=service_with_stats, + day=date.today() + ) + auth_header = create_authorization_header( + service_id=service_with_stats.id + ) + + response = client.get( + '/notifications/statistics?day={}'.format(date.today().isoformat()), + headers=[auth_header] + ) + + notifications = json.loads(response.get_data(as_text=True)) + + assert len(notifications['data']) == 2 + retrieved_stats = notifications['data'][0] + generated_stats = notifications['data'][1] + + assert retrieved_stats['emails_requested'] == 2 + assert retrieved_stats['emails_delivered'] == 1 + assert retrieved_stats['emails_failed'] == 1 + assert retrieved_stats['sms_requested'] == 2 + assert retrieved_stats['sms_delivered'] == 1 + assert retrieved_stats['sms_failed'] == 1 + assert retrieved_stats['service'] == str(service_with_stats.id) + + assert generated_stats['emails_requested'] == 0 + assert generated_stats['emails_delivered'] == 0 + assert generated_stats['emails_failed'] == 0 + assert generated_stats['sms_requested'] == 0 + assert generated_stats['sms_delivered'] == 0 + assert generated_stats['sms_failed'] == 0 + assert generated_stats['service'] == str(service_without_stats.id) + + assert response.status_code == 200