mirror of
https://github.com/GSA/notifications-api.git
synced 2026-07-21 08:30:00 -04:00
Updated backend to fix a few bugs and keep things dry. Added unit tests
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import calendar
|
||||
import pytz
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
from app.utils import utc_now
|
||||
@@ -93,3 +95,25 @@ def generate_date_range(start_date, end_date=None, days=0):
|
||||
current_date += timedelta(days=1)
|
||||
else:
|
||||
return "An end_date or number of days must be specified"
|
||||
|
||||
|
||||
def build_local_and_utc_date_range(
|
||||
start_date_str: str,
|
||||
days: int = 7,
|
||||
timezone: str = "UTC"
|
||||
):
|
||||
"""
|
||||
Convert date to local range based on timezone
|
||||
"""
|
||||
|
||||
user_timezone = pytz.timezone(timezone)
|
||||
local_end_date = datetime.strptime(start_date_str, "%Y-%m-%d").replace(tzinfo=user_timezone)
|
||||
# Subtract (days - 1) so the entire final day is included in the range
|
||||
local_start_date = local_end_date - timedelta(days=days)
|
||||
|
||||
|
||||
# Convert to UTC for database queries
|
||||
utc_start_date = local_start_date.astimezone(pytz.utc).replace(hour=0, minute=0, second=0)
|
||||
utc_end_date = local_end_date.astimezone(pytz.utc).replace(hour=23, minute=59, second=59)
|
||||
|
||||
return (local_start_date, utc_start_date, utc_end_date)
|
||||
|
||||
@@ -503,7 +503,7 @@ def dao_fetch_stats_for_service_from_days_for_user(
|
||||
select(
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at).label("day"),
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at).label("timestamp"),
|
||||
func.count(NotificationAllTimeView.id).label("count"),
|
||||
)
|
||||
.select_from(NotificationAllTimeView)
|
||||
|
||||
@@ -20,7 +20,7 @@ from app.dao.api_key_dao import (
|
||||
save_model_api_key,
|
||||
)
|
||||
from app.dao.dao_utils import dao_rollback, transaction
|
||||
from app.dao.date_util import get_calendar_year, get_month_start_and_end_date_in_utc
|
||||
from app.dao.date_util import build_local_and_utc_date_range, get_calendar_year, get_month_start_and_end_date_in_utc
|
||||
from app.dao.fact_notification_status_dao import (
|
||||
fetch_monthly_template_usage_for_service,
|
||||
fetch_notification_status_for_service_by_month,
|
||||
@@ -221,72 +221,65 @@ def get_service_notification_statistics(service_id):
|
||||
|
||||
@service_blueprint.route("/<uuid:service_id>/statistics/<string:start>/<int:days>")
|
||||
def get_service_notification_statistics_by_day(service_id, start, days):
|
||||
# Allow timezone override, default to UTC
|
||||
user_timezone = request.args.get("timezone", "UTC")
|
||||
|
||||
return jsonify(
|
||||
data=get_service_statistics_for_specific_days(service_id, start, int(days), user_timezone)
|
||||
)
|
||||
|
||||
|
||||
def convert_local_to_utc(local_dt, user_timezone="America/New_York"):
|
||||
local_timezone = pytz.timezone(user_timezone)
|
||||
if local_dt.tzinfo is None:
|
||||
localized_dt = local_timezone.localize(local_dt)
|
||||
else:
|
||||
localized_dt = local_dt.astimezone(local_timezone)
|
||||
|
||||
return localized_dt.astimezone(pytz.utc)
|
||||
|
||||
|
||||
def get_service_statistics_for_specific_days(service_id, start, days=7, timezone="UTC"):
|
||||
user_timezone = pytz.timezone(timezone)
|
||||
local_end_date = datetime.strptime(start, "%Y-%m-%d").replace(tzinfo=user_timezone)
|
||||
local_start_date = local_end_date - timedelta(days=days - 1)
|
||||
|
||||
utc_start_date = local_start_date.astimezone(pytz.utc).replace(hour=0, minute=0, second=0)
|
||||
utc_end_date = local_end_date.astimezone(pytz.utc).replace(hour=23, minute=59, second=59)
|
||||
|
||||
now_utc = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
now_local = now_utc.astimezone(user_timezone)
|
||||
|
||||
# If the local day hasn't fully ended in UTC yet, extend the end date to the next day
|
||||
if now_local.date() == local_end_date.date() and now_utc.date() > now_local.date():
|
||||
utc_end_date += timedelta(days=1)
|
||||
(
|
||||
local_start_date,
|
||||
utc_start_date,
|
||||
utc_end_date
|
||||
) = build_local_and_utc_date_range(start, days, timezone)
|
||||
|
||||
results = dao_fetch_stats_for_service_from_days(service_id, utc_start_date, utc_end_date)
|
||||
stats = get_specific_days_stats(results, utc_start_date, days=days, timezone=timezone)
|
||||
|
||||
stats = get_specific_days_stats(
|
||||
data=results,
|
||||
start_date=local_start_date,
|
||||
days=days,
|
||||
timezone=timezone
|
||||
)
|
||||
return stats
|
||||
|
||||
|
||||
|
||||
@service_blueprint.route(
|
||||
"/<uuid:service_id>/statistics/user/<uuid:user_id>/<string:start>/<int:days>"
|
||||
)
|
||||
def get_service_notification_statistics_by_day_by_user(
|
||||
service_id, user_id, start, days
|
||||
):
|
||||
def get_service_notification_statistics_by_day_by_user(service_id, user_id, start, days):
|
||||
user_timezone = request.args.get('timezone', 'UTC')
|
||||
return jsonify(
|
||||
data=get_service_statistics_for_specific_days_by_user(
|
||||
service_id, user_id, start, int(days)
|
||||
service_id, user_id, start, int(days), timezone=user_timezone
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_service_statistics_for_specific_days_by_user(
|
||||
service_id, user_id, start, days=1
|
||||
service_id, user_id, start, days=1, timezone="UTC"
|
||||
):
|
||||
# start and end dates needs to be reversed because
|
||||
# the end date is today and the start is x days in the past
|
||||
# a day needs to be substracted to allow for today
|
||||
end_date = datetime.strptime(start, "%Y-%m-%d")
|
||||
start_date = end_date - timedelta(days=days - 1)
|
||||
(
|
||||
local_start_date,
|
||||
utc_start_date,
|
||||
utc_end_date
|
||||
) = build_local_and_utc_date_range(start_date_str=start, days=days, timezone=timezone)
|
||||
|
||||
results = dao_fetch_stats_for_service_from_days_for_user(
|
||||
service_id, start_date, end_date, user_id
|
||||
service_id,
|
||||
utc_start_date,
|
||||
utc_end_date,
|
||||
user_id
|
||||
)
|
||||
|
||||
stats = get_specific_days_stats(results, start_date, days=days)
|
||||
stats = get_specific_days_stats(
|
||||
data=results,
|
||||
start_date=local_start_date,
|
||||
days=days,
|
||||
timezone=timezone
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
import pytz
|
||||
|
||||
import pytest
|
||||
|
||||
from app.dao.date_util import (
|
||||
build_local_and_utc_date_range,
|
||||
get_calendar_year,
|
||||
get_calendar_year_for_datetime,
|
||||
get_month_start_and_end_date_in_utc,
|
||||
@@ -75,3 +77,31 @@ def test_get_month_start_and_end_date_in_utc(month, year, expected_start, expect
|
||||
)
|
||||
def test_get_calendar_year_for_datetime(dt, fy):
|
||||
assert get_calendar_year_for_datetime(dt) == fy
|
||||
|
||||
|
||||
def test_build_local_and_utc_date_range():
|
||||
local_start, utc_start, utc_end = build_local_and_utc_date_range(
|
||||
"2025-02-04", 7, "America/New_York"
|
||||
)
|
||||
assert local_start.tzinfo
|
||||
assert utc_start.tzinfo
|
||||
assert utc_end.tzinfo
|
||||
assert utc_end > utc_start
|
||||
|
||||
|
||||
def test_build_local_and_utc_7_days_ny():
|
||||
local_start, utc_start, utc_end = build_local_and_utc_date_range(
|
||||
"2025-02-10", 7, "America/New_York"
|
||||
)
|
||||
diff = local_start + timedelta(days=7)
|
||||
assert diff == datetime(2025, 2, 10, tzinfo=pytz.timezone("America/New_York"))
|
||||
assert utc_start < utc_end
|
||||
|
||||
def test_build_local_and_utc_1_day_utc():
|
||||
local_start, utc_start, utc_end = build_local_and_utc_date_range(
|
||||
"2025-02-10", 1, "UTC"
|
||||
)
|
||||
# this should be one day before
|
||||
assert local_start.isoformat() == "2025-02-09T00:00:00+00:00"
|
||||
assert utc_start.hour == 0
|
||||
assert utc_end.hour == 23
|
||||
|
||||
Reference in New Issue
Block a user