mirror of
https://github.com/GSA/notifications-api.git
synced 2026-01-31 15:15:38 -05:00
Merge pull request #1059 from GSA/1006-new-api-failed-and-delivered-messages-7-days
1006/1007/1008 created new API endpoints for front end statistics
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import calendar
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
from app.utils import utc_now
|
||||
@@ -66,3 +67,29 @@ def get_calendar_year_for_datetime(start_date):
|
||||
return year - 1
|
||||
else:
|
||||
return year
|
||||
|
||||
|
||||
def get_number_of_days_for_month(year, month):
|
||||
return calendar.monthrange(year, month)[1]
|
||||
|
||||
|
||||
def generate_date_range(start_date, end_date=None, days=0):
|
||||
if end_date:
|
||||
current_date = start_date
|
||||
while current_date <= end_date:
|
||||
try:
|
||||
yield current_date.date()
|
||||
except ValueError:
|
||||
pass
|
||||
current_date += timedelta(days=1)
|
||||
elif days > 0:
|
||||
end_date = start_date + timedelta(days=days)
|
||||
current_date = start_date
|
||||
while current_date < end_date:
|
||||
try:
|
||||
yield current_date.date()
|
||||
except ValueError:
|
||||
pass
|
||||
current_date += timedelta(days=1)
|
||||
else:
|
||||
return "An end_date or number of days must be specified"
|
||||
|
||||
@@ -84,21 +84,21 @@ def update_fact_notification_status(process_day, notification_type, service_id):
|
||||
def fetch_notification_status_for_service_by_month(start_date, end_date, service_id):
|
||||
return (
|
||||
db.session.query(
|
||||
func.date_trunc("month", FactNotificationStatus.local_date).label("month"),
|
||||
FactNotificationStatus.notification_type,
|
||||
FactNotificationStatus.notification_status,
|
||||
func.sum(FactNotificationStatus.notification_count).label("count"),
|
||||
func.date_trunc("month", NotificationAllTimeView.created_at).label("month"),
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status.label("notification_status"),
|
||||
func.count(NotificationAllTimeView.id).label("count"),
|
||||
)
|
||||
.filter(
|
||||
FactNotificationStatus.service_id == service_id,
|
||||
FactNotificationStatus.local_date >= start_date,
|
||||
FactNotificationStatus.local_date < end_date,
|
||||
FactNotificationStatus.key_type != KeyType.TEST,
|
||||
NotificationAllTimeView.service_id == service_id,
|
||||
NotificationAllTimeView.created_at >= start_date,
|
||||
NotificationAllTimeView.created_at < end_date,
|
||||
NotificationAllTimeView.key_type != KeyType.TEST,
|
||||
)
|
||||
.group_by(
|
||||
func.date_trunc("month", FactNotificationStatus.local_date).label("month"),
|
||||
FactNotificationStatus.notification_type,
|
||||
FactNotificationStatus.notification_status,
|
||||
func.date_trunc("month", NotificationAllTimeView.created_at).label("month"),
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.sql.expression import and_, asc, case, func
|
||||
|
||||
from app import db
|
||||
from app.dao.dao_utils import VersionOptions, autocommit, version_class
|
||||
from app.dao.date_util import get_current_calendar_year
|
||||
from app.dao.date_util import generate_date_range, get_current_calendar_year
|
||||
from app.dao.organization_dao import dao_get_organization_by_email_address
|
||||
from app.dao.service_sms_sender_dao import insert_service_sms_sender
|
||||
from app.dao.service_user_dao import dao_get_service_user
|
||||
@@ -27,6 +27,7 @@ from app.models import (
|
||||
InvitedUser,
|
||||
Job,
|
||||
Notification,
|
||||
NotificationAllTimeView,
|
||||
NotificationHistory,
|
||||
Organization,
|
||||
Permission,
|
||||
@@ -40,6 +41,7 @@ from app.models import (
|
||||
User,
|
||||
VerifyCode,
|
||||
)
|
||||
from app.service import statistics
|
||||
from app.utils import (
|
||||
escape_special_characters,
|
||||
get_archived_db_column_value,
|
||||
@@ -426,6 +428,61 @@ def dao_fetch_todays_stats_for_service(service_id):
|
||||
)
|
||||
|
||||
|
||||
def dao_fetch_stats_for_service_from_days(service_id, start_date, end_date):
|
||||
start_date = get_midnight_in_utc(start_date)
|
||||
end_date = get_midnight_in_utc(end_date + timedelta(days=1))
|
||||
|
||||
return (
|
||||
db.session.query(
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at).label("day"),
|
||||
func.count(NotificationAllTimeView.id).label("count"),
|
||||
)
|
||||
.filter(
|
||||
NotificationAllTimeView.service_id == service_id,
|
||||
NotificationAllTimeView.key_type != KeyType.TEST,
|
||||
NotificationAllTimeView.created_at >= start_date,
|
||||
NotificationAllTimeView.created_at < end_date,
|
||||
)
|
||||
.group_by(
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def dao_fetch_stats_for_service_from_days_for_user(
|
||||
service_id, start_date, end_date, user_id
|
||||
):
|
||||
start_date = get_midnight_in_utc(start_date)
|
||||
end_date = get_midnight_in_utc(end_date + timedelta(days=1))
|
||||
|
||||
return (
|
||||
db.session.query(
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at).label("day"),
|
||||
func.count(NotificationAllTimeView.id).label("count"),
|
||||
)
|
||||
.filter(
|
||||
NotificationAllTimeView.service_id == service_id,
|
||||
NotificationAllTimeView.key_type != KeyType.TEST,
|
||||
NotificationAllTimeView.created_at >= start_date,
|
||||
NotificationAllTimeView.created_at < end_date,
|
||||
NotificationAllTimeView.created_by_id == user_id,
|
||||
)
|
||||
.group_by(
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
func.date_trunc("day", NotificationAllTimeView.created_at),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def dao_fetch_todays_stats_for_all_services(
|
||||
include_from_test_key=True, only_active=True
|
||||
):
|
||||
@@ -607,3 +664,52 @@ def get_live_services_with_organization():
|
||||
)
|
||||
|
||||
return query.all()
|
||||
|
||||
|
||||
def fetch_notification_stats_for_service_by_month_by_user(
|
||||
start_date, end_date, service_id, user_id
|
||||
):
|
||||
return (
|
||||
db.session.query(
|
||||
func.date_trunc("month", NotificationAllTimeView.created_at).label("month"),
|
||||
NotificationAllTimeView.notification_type,
|
||||
(NotificationAllTimeView.status).label("notification_status"),
|
||||
func.count(NotificationAllTimeView.id).label("count"),
|
||||
)
|
||||
.filter(
|
||||
NotificationAllTimeView.service_id == service_id,
|
||||
NotificationAllTimeView.created_at >= start_date,
|
||||
NotificationAllTimeView.created_at < end_date,
|
||||
NotificationAllTimeView.key_type != KeyType.TEST,
|
||||
NotificationAllTimeView.created_by_id == user_id,
|
||||
)
|
||||
.group_by(
|
||||
func.date_trunc("month", NotificationAllTimeView.created_at).label("month"),
|
||||
NotificationAllTimeView.notification_type,
|
||||
NotificationAllTimeView.status,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def get_specific_days_stats(results, start_date, days=None, end_date=None):
|
||||
if days is not None and end_date is not None:
|
||||
raise ValueError("Only set days OR set end_date, not both.")
|
||||
elif days is not None:
|
||||
gen_range = generate_date_range(start_date, days=days)
|
||||
elif end_date is not None:
|
||||
gen_range = generate_date_range(start_date, end_date)
|
||||
else:
|
||||
raise ValueError("Either days or end_date must be set.")
|
||||
|
||||
grouped_results = {date: [] for date in gen_range} | {
|
||||
day.date(): [notification_type, status, day, count]
|
||||
for notification_type, status, day, count in results
|
||||
}
|
||||
|
||||
stats = {
|
||||
day.strftime("%Y-%m-%d"): statistics.format_statistics(rows)
|
||||
for day, rows in grouped_results.items()
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import itertools
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import Blueprint, current_app, jsonify, request
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@@ -17,7 +17,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
|
||||
from app.dao.date_util import 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,
|
||||
@@ -63,13 +63,17 @@ from app.dao.services_dao import (
|
||||
dao_fetch_all_services_by_user,
|
||||
dao_fetch_live_services_data,
|
||||
dao_fetch_service_by_id,
|
||||
dao_fetch_stats_for_service_from_days,
|
||||
dao_fetch_stats_for_service_from_days_for_user,
|
||||
dao_fetch_todays_stats_for_all_services,
|
||||
dao_fetch_todays_stats_for_service,
|
||||
dao_remove_user_from_service,
|
||||
dao_resume_service,
|
||||
dao_suspend_service,
|
||||
dao_update_service,
|
||||
fetch_notification_stats_for_service_by_month_by_user,
|
||||
get_services_by_partial_name,
|
||||
get_specific_days_stats,
|
||||
)
|
||||
from app.dao.templates_dao import dao_get_template_by_id
|
||||
from app.dao.users_dao import get_user_by_id
|
||||
@@ -210,6 +214,58 @@ 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):
|
||||
return jsonify(
|
||||
data=get_service_statistics_for_specific_days(service_id, start, int(days))
|
||||
)
|
||||
|
||||
|
||||
def get_service_statistics_for_specific_days(service_id, start, days=1):
|
||||
# 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)
|
||||
|
||||
results = dao_fetch_stats_for_service_from_days(service_id, start_date, end_date)
|
||||
|
||||
stats = get_specific_days_stats(results, start_date, days=days)
|
||||
|
||||
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
|
||||
):
|
||||
return jsonify(
|
||||
data=get_service_statistics_for_specific_days_by_user(
|
||||
service_id, user_id, start, int(days)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_service_statistics_for_specific_days_by_user(
|
||||
service_id, user_id, start, days=1
|
||||
):
|
||||
# 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)
|
||||
|
||||
results = dao_fetch_stats_for_service_from_days_for_user(
|
||||
service_id, start_date, end_date, user_id
|
||||
)
|
||||
|
||||
stats = get_specific_days_stats(results, start_date, days=days)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@service_blueprint.route("", methods=["POST"])
|
||||
def create_service():
|
||||
data = request.get_json()
|
||||
@@ -592,6 +648,7 @@ def get_monthly_notification_stats(service_id):
|
||||
stats = fetch_notification_status_for_service_by_month(
|
||||
start_date, end_date, service_id
|
||||
)
|
||||
|
||||
statistics.add_monthly_notification_status_stats(data, stats)
|
||||
|
||||
now = utc_now()
|
||||
@@ -604,6 +661,87 @@ def get_monthly_notification_stats(service_id):
|
||||
return jsonify(data=data)
|
||||
|
||||
|
||||
@service_blueprint.route(
|
||||
"/<uuid:service_id>/notifications/<uuid:user_id>/monthly", methods=["GET"]
|
||||
)
|
||||
def get_monthly_notification_stats_by_user(service_id, user_id):
|
||||
# check service_id validity
|
||||
dao_fetch_service_by_id(service_id)
|
||||
# user = get_user_by_id(user_id=user_id)
|
||||
|
||||
try:
|
||||
year = int(request.args.get("year", "NaN"))
|
||||
except ValueError:
|
||||
raise InvalidRequest("Year must be a number", status_code=400)
|
||||
|
||||
start_date, end_date = get_calendar_year(year)
|
||||
|
||||
data = statistics.create_empty_monthly_notification_status_stats_dict(year)
|
||||
|
||||
stats = fetch_notification_stats_for_service_by_month_by_user(
|
||||
start_date, end_date, service_id, user_id
|
||||
)
|
||||
|
||||
statistics.add_monthly_notification_status_stats(data, stats)
|
||||
|
||||
now = utc_now()
|
||||
if end_date > now:
|
||||
todays_deltas = fetch_notification_status_for_service_for_day(
|
||||
now, service_id=service_id
|
||||
)
|
||||
statistics.add_monthly_notification_status_stats(data, todays_deltas)
|
||||
|
||||
return jsonify(data=data)
|
||||
|
||||
|
||||
@service_blueprint.route(
|
||||
"/<uuid:service_id>/notifications/<uuid:user_id>/month", methods=["GET"]
|
||||
)
|
||||
def get_single_month_notification_stats_by_user(service_id, user_id):
|
||||
# check service_id validity
|
||||
dao_fetch_service_by_id(service_id)
|
||||
|
||||
try:
|
||||
month = int(request.args.get("month", "NaN"))
|
||||
year = int(request.args.get("year", "NaN"))
|
||||
except ValueError:
|
||||
raise InvalidRequest(
|
||||
"Both a month and year are required as numbers", status_code=400
|
||||
)
|
||||
|
||||
month_year = datetime(year, month, 10, 00, 00, 00)
|
||||
start_date, end_date = get_month_start_and_end_date_in_utc(month_year)
|
||||
|
||||
results = dao_fetch_stats_for_service_from_days_for_user(
|
||||
service_id, start_date, end_date, user_id
|
||||
)
|
||||
|
||||
stats = get_specific_days_stats(results, start_date, end_date=end_date)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@service_blueprint.route("/<uuid:service_id>/notifications/month", methods=["GET"])
|
||||
def get_single_month_notification_stats_for_service(service_id):
|
||||
# check service_id validity
|
||||
dao_fetch_service_by_id(service_id)
|
||||
|
||||
try:
|
||||
month = int(request.args.get("month", "NaN"))
|
||||
year = int(request.args.get("year", "NaN"))
|
||||
except ValueError:
|
||||
raise InvalidRequest(
|
||||
"Both a month and year are required as numbers", status_code=400
|
||||
)
|
||||
|
||||
month_year = datetime(year, month, 10, 00, 00, 00)
|
||||
start_date, end_date = get_month_start_and_end_date_in_utc(month_year)
|
||||
|
||||
results = dao_fetch_stats_for_service_from_days(service_id, start_date, end_date)
|
||||
|
||||
stats = get_specific_days_stats(results, start_date, end_date=end_date)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
def get_detailed_service(service_id, today_only=False):
|
||||
service = dao_fetch_service_by_id(service_id)
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ def create_empty_monthly_notification_status_stats_dict(year):
|
||||
def add_monthly_notification_status_stats(data, stats):
|
||||
for row in stats:
|
||||
month = row.month.strftime("%Y-%m")
|
||||
|
||||
data[month][row.notification_type][row.notification_status] += row.count
|
||||
|
||||
data[month][row.notification_type][StatisticsType.REQUESTED] += row.count
|
||||
return data
|
||||
|
||||
Reference in New Issue
Block a user