diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index 863191973..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, @@ -57,6 +58,51 @@ def dao_get_notification_statistics_for_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 48c65d93b..679a3cdfc 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -28,7 +28,8 @@ from app.schemas import ( sms_template_notification_schema, notification_status_schema, notifications_filter_schema, - notifications_statistics_schema + notifications_statistics_schema, + day_schema, ) from app.celery.tasks import send_sms, send_email @@ -389,14 +390,12 @@ def send_notification(notification_type): @notifications.route('/notifications/statistics') def get_notification_statistics_for_day(): - data, errors = notifications_statistics_schema.load(request.args) + data, errors = day_schema.load(request.args) if errors: return jsonify(result='error', message=errors), 400 - if not data.day: - return jsonify(result='error', message='Please provide day as query parameter.'), 400 - statistics = notifications_dao.dao_get_notification_statistics_for_day( - day=data.day + statistics = notifications_dao.dao_get_potential_notification_statistics_for_day( + day=data['day'] ) data, errors = notifications_statistics_schema.dump(statistics, many=True) 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/test_notification_statistics.py b/tests/app/notifications/rest/test_notification_statistics.py index 2d6ab47db..ec7a0ba0f 100644 --- a/tests/app/notifications/rest/test_notification_statistics.py +++ b/tests/app/notifications/rest/test_notification_statistics.py @@ -4,7 +4,10 @@ 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 +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): @@ -28,6 +31,7 @@ def test_get_notification_statistics(notify_api, sample_notification_statistics) 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 @@ -85,6 +89,7 @@ def test_get_notification_statistics_fails_if_no_date(notify_api, sample_notific 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 @@ -102,4 +107,90 @@ def test_get_notification_statistics_fails_if_invalid_date(notify_api, sample_no 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