rewrite weekly aggregate function to honor week boundaries

(and not use the stats table, and also be easier to read)
This commit is contained in:
Leo Hemsted
2016-07-26 11:00:03 +01:00
parent 48eff9a2ee
commit 444132ad66
3 changed files with 129 additions and 5 deletions

View File

@@ -157,3 +157,21 @@ def _stats_for_service_query(service_id):
Notification.notification_type, Notification.notification_type,
Notification.status, Notification.status,
) )
def dao_fetch_weekly_historical_stats_for_service(service_id):
monday_of_notification_week = func.date_trunc('week', NotificationHistory.created_at).label('week_start')
return db.session.query(
NotificationHistory.notification_type,
NotificationHistory.status,
monday_of_notification_week,
func.count(NotificationHistory.id).label('count')
).filter(
NotificationHistory.service_id == service_id
).group_by(
NotificationHistory.notification_type,
NotificationHistory.status,
monday_of_notification_week
).order_by(
asc(monday_of_notification_week), NotificationHistory.status
).all()

View File

@@ -13,6 +13,7 @@ from app.models import (
ApiKey, ApiKey,
Job, Job,
Notification, Notification,
NotificationHistory,
InvitedUser, InvitedUser,
Permission, Permission,
ProviderStatistics, ProviderStatistics,
@@ -42,6 +43,8 @@ def service_factory(notify_db, notify_db_session):
def get(self, service_name, user=None, template_type=None, email_from=None): def get(self, service_name, user=None, template_type=None, email_from=None):
if not user: if not user:
user = sample_user(notify_db, notify_db_session) user = sample_user(notify_db, notify_db_session)
if not email_from:
email_from = service_name
service = sample_service(notify_db, notify_db_session, service_name, user, email_from=email_from) service = sample_service(notify_db, notify_db_session, service_name, user, email_from=email_from)
if template_type == 'email': if template_type == 'email':
sample_template( sample_template(
@@ -367,6 +370,31 @@ def sample_notification(notify_db,
return notification return notification
@pytest.fixture(scope='function')
def sample_notification_history(notify_db,
notify_db_session,
sample_template,
status='created',
created_at=None):
if created_at is None:
created_at = datetime.utcnow()
notification_history = NotificationHistory(
id=uuid.uuid4(),
service=sample_template.service,
template=sample_template,
template_version=sample_template.version,
status=status,
created_at=created_at,
notification_type=sample_template.template_type,
key_type=KEY_TYPE_NORMAL
)
notify_db.session.add(notification_history)
notify_db.session.commit()
return notification_history
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def mock_celery_send_sms_code(mocker): def mock_celery_send_sms_code(mocker):
return mocker.patch('app.celery.tasks.send_sms_code.apply_async') return mocker.patch('app.celery.tasks.send_sms_code.apply_async')

View File

@@ -1,6 +1,8 @@
from datetime import datetime
import uuid import uuid
import pytest import functools
import pytest
from sqlalchemy.orm.exc import FlushError, NoResultFound from sqlalchemy.orm.exc import FlushError, NoResultFound
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from freezegun import freeze_time from freezegun import freeze_time
@@ -16,7 +18,8 @@ from app.dao.services_dao import (
dao_update_service, dao_update_service,
delete_service_and_all_associated_db_objects, delete_service_and_all_associated_db_objects,
dao_fetch_stats_for_service, dao_fetch_stats_for_service,
dao_fetch_todays_stats_for_service dao_fetch_todays_stats_for_service,
dao_fetch_weekly_historical_stats_for_service
) )
from app.dao.users_dao import save_model_user from app.dao.users_dao import save_model_user
from app.models import ( from app.models import (
@@ -36,7 +39,8 @@ from app.models import (
) )
from tests.app.conftest import ( from tests.app.conftest import (
sample_notification as create_notification sample_notification as create_notification,
sample_notification_history as create_notification_history
) )
@@ -407,7 +411,7 @@ def test_fetch_stats_counts_correctly(notify_db, notify_db_session, sample_templ
create_notification(notify_db, notify_db_session, template=sample_email_template, status='technical-failure') create_notification(notify_db, notify_db_session, template=sample_email_template, status='technical-failure')
create_notification(notify_db, notify_db_session, template=sample_template, status='created') create_notification(notify_db, notify_db_session, template=sample_template, status='created')
stats = dao_fetch_stats_for_service(sample_template.service.id) stats = dao_fetch_stats_for_service(sample_template.service_id)
stats = sorted(stats, key=lambda x: (x.notification_type, x.status)) stats = sorted(stats, key=lambda x: (x.notification_type, x.status))
assert len(stats) == 3 assert len(stats) == 3
@@ -435,9 +439,83 @@ def test_fetch_stats_for_today_only_includes_today(notify_db, notify_db_session,
with freeze_time('2001-01-02T12:00:00'): with freeze_time('2001-01-02T12:00:00'):
right_now = create_notification(notify_db, None, to_field='3', status='created') right_now = create_notification(notify_db, None, to_field='3', status='created')
stats = dao_fetch_todays_stats_for_service(sample_template.service.id) stats = dao_fetch_todays_stats_for_service(sample_template.service_id)
stats = {row.status: row.count for row in stats} stats = {row.status: row.count for row in stats}
assert 'delivered' not in stats assert 'delivered' not in stats
assert stats['failed'] == 1 assert stats['failed'] == 1
assert stats['created'] == 1 assert stats['created'] == 1
def test_fetch_weekly_historical_stats_separates_weeks(notify_db, notify_db_session, sample_template):
notification_history = functools.partial(
create_notification_history,
notify_db,
notify_db_session,
sample_template
)
week_53_last_yr = notification_history(created_at=datetime(2016, 1, 1))
week_1_last_yr = notification_history(created_at=datetime(2016, 1, 5))
last_sunday = notification_history(created_at=datetime(2016, 7, 24, 23, 59))
last_monday_morning = notification_history(created_at=datetime(2016, 7, 25, 0, 0))
last_monday_evening = notification_history(created_at=datetime(2016, 7, 25, 23, 59))
today = notification_history(created_at=datetime.now(), status='delivered')
with freeze_time('Wed 27th July 2016'):
ret = dao_fetch_weekly_historical_stats_for_service(sample_template.service_id)
assert [(row.week_start, row.status) for row in ret] == [
(datetime(2015, 12, 28), 'created'),
(datetime(2016, 1, 4), 'created'),
(datetime(2016, 7, 18), 'created'),
(datetime(2016, 7, 25), 'created'),
(datetime(2016, 7, 25), 'delivered')
]
assert ret[-2].count == 2
assert ret[-1].count == 1
def test_fetch_weekly_historical_stats_ignores_second_service(notify_db, notify_db_session, service_factory):
template_1 = service_factory.get('1').templates[0]
template_2 = service_factory.get('2').templates[0]
notification_history = functools.partial(
create_notification_history,
notify_db,
notify_db_session
)
last_sunday = notification_history(template_1, created_at=datetime(2016, 7, 24, 23, 59))
last_monday_morning = notification_history(template_2, created_at=datetime(2016, 7, 25, 0, 0))
with freeze_time('Wed 27th July 2016'):
ret = dao_fetch_weekly_historical_stats_for_service(template_1.service_id)
assert len(ret) == 1
assert ret[0].week_start == datetime(2016, 7, 18)
assert ret[0].count == 1
def test_fetch_weekly_historical_stats_separates_types(notify_db,
notify_db_session,
sample_template,
sample_email_template):
notification_history = functools.partial(
create_notification_history,
notify_db,
notify_db_session,
created_at=datetime(2016, 7, 25)
)
notification_history(sample_template)
notification_history(sample_email_template)
with freeze_time('Wed 27th July 2016'):
ret = dao_fetch_weekly_historical_stats_for_service(sample_template.service_id)
assert len(ret) == 2
assert ret[0].week_start == datetime(2016, 7, 25)
assert ret[0].count == 1
assert ret[0].notification_type == 'email'
assert ret[1].week_start == datetime(2016, 7, 25)
assert ret[1].count == 1
assert ret[1].notification_type == 'sms'