Standardise how we query midnight-to-midnight

Partially addresses [1] (lots more detail to read in the comment).
I've also added some tests for the status DAO function to confirm
it behaves as expected across timezones.

[1]: https://github.com/alphagov/notifications-api/pull/3437#discussion_r802634913
This commit is contained in:
Ben Thorner
2022-02-09 17:44:00 +00:00
parent 7f4b140f97
commit 6e8f121548
4 changed files with 30 additions and 12 deletions

View File

@@ -1,7 +1,7 @@
from datetime import date, datetime, time, timedelta from datetime import date, datetime, timedelta
from flask import current_app from flask import current_app
from notifications_utils.timezones import convert_bst_to_utc, convert_utc_to_bst from notifications_utils.timezones import convert_utc_to_bst
from sqlalchemy import Date, Integer, and_, desc, func from sqlalchemy import Date, Integer, and_, desc, func
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.sql.expression import case, literal from sqlalchemy.sql.expression import case, literal
@@ -317,8 +317,8 @@ def delete_billing_data_for_service_for_day(process_day, service_id):
def fetch_billing_data_for_day(process_day, service_id=None, check_permissions=False): def fetch_billing_data_for_day(process_day, service_id=None, check_permissions=False):
start_date = convert_bst_to_utc(datetime.combine(process_day, time.min)) start_date = get_london_midnight_in_utc(process_day)
end_date = convert_bst_to_utc(datetime.combine(process_day + timedelta(days=1), time.min)) end_date = get_london_midnight_in_utc(process_day + timedelta(days=1))
current_app.logger.info("Populate ft_billing for {} to {}".format(start_date, end_date)) current_app.logger.info("Populate ft_billing for {} to {}".format(start_date, end_date))
transit_data = [] transit_data = []
if not service_id: if not service_id:

View File

@@ -1,6 +1,5 @@
from datetime import datetime, time, timedelta from datetime import datetime, timedelta
from notifications_utils.timezones import convert_bst_to_utc
from sqlalchemy import Date, case, func from sqlalchemy import Date, case, func
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.sql.expression import extract, literal from sqlalchemy.sql.expression import extract, literal
@@ -36,8 +35,8 @@ from app.utils import (
def fetch_status_data_for_service_and_day(process_day, service_id, notification_type): def fetch_status_data_for_service_and_day(process_day, service_id, notification_type):
start_date = convert_bst_to_utc(datetime.combine(process_day, time.min)) start_date = get_london_midnight_in_utc(process_day)
end_date = convert_bst_to_utc(datetime.combine(process_day + timedelta(days=1), time.min)) end_date = get_london_midnight_in_utc(process_day + timedelta(days=1))
# query notifications or notification_history for the day, depending on their data retention # query notifications or notification_history for the day, depending on their data retention
service = Service.query.get(service_id) service = Service.query.get(service_id)

View File

@@ -8,7 +8,7 @@ from notifications_utils.template import (
LetterPrintTemplate, LetterPrintTemplate,
SMSMessageTemplate, SMSMessageTemplate,
) )
from notifications_utils.timezones import convert_utc_to_bst from notifications_utils.timezones import convert_bst_to_utc, convert_utc_to_bst
from sqlalchemy import func from sqlalchemy import func
DATETIME_FORMAT_NO_TIMEZONE = "%Y-%m-%d %H:%M:%S.%f" DATETIME_FORMAT_NO_TIMEZONE = "%Y-%m-%d %H:%M:%S.%f"
@@ -64,9 +64,7 @@ def get_london_midnight_in_utc(date):
:param date: the day to calculate the London midnight in UTC for :param date: the day to calculate the London midnight in UTC for
:return: the datetime of London midnight in UTC, for example 2016-06-17 = 2016-06-16 23:00:00 :return: the datetime of London midnight in UTC, for example 2016-06-17 = 2016-06-16 23:00:00
""" """
return local_timezone.localize(datetime.combine(date, datetime.min.time())).astimezone( return convert_bst_to_utc(datetime.combine(date, datetime.min.time()))
pytz.UTC).replace(
tzinfo=None)
def get_midnight_for_day_before(date): def get_midnight_for_day_before(date):

View File

@@ -14,6 +14,7 @@ from app.dao.fact_notification_status_dao import (
fetch_notification_status_totals_for_all_services, fetch_notification_status_totals_for_all_services,
fetch_notification_statuses_for_job, fetch_notification_statuses_for_job,
fetch_stats_for_all_services_by_date_range, fetch_stats_for_all_services_by_date_range,
fetch_status_data_for_service_and_day,
get_total_notifications_for_date_range, get_total_notifications_for_date_range,
) )
from app.models import ( from app.models import (
@@ -607,3 +608,23 @@ def test_get_total_notifications_for_date_range(sample_service):
assert len(results) == 1 assert len(results) == 1
assert results[0] == ("2021-03-01", 15, 20, 3) assert results[0] == ("2021-03-01", 15, 20, 3)
@pytest.mark.parametrize('created_at_utc,process_day,expected_count', [
# Clocks change on the 27th of March 2022, so the query needs to look at the
# time range 00:00 - 23:00 (UTC) thereafter.
('2022-03-27T00:30', date(2022, 3, 27), 1), # 27/03 00:30 GMT
('2022-03-27T22:30', date(2022, 3, 27), 1), # 27/03 23:30 BST
('2022-03-27T23:30', date(2022, 3, 27), 0), # 28/03 00:30 BST
('2022-03-26T23:30', date(2022, 3, 26), 1), # 26/03 23:30 GMT
])
def test_fetch_status_data_for_service_and_day_respects_gmt_bst(
sample_template,
sample_service,
created_at_utc,
process_day,
expected_count,
):
create_notification(template=sample_template, created_at=created_at_utc)
rows = fetch_status_data_for_service_and_day(process_day, sample_service.id, SMS_TYPE)
assert len(rows) == expected_count