diff --git a/app/enums.py b/app/enums.py index 405c28cdf..025a5c50b 100644 --- a/app/enums.py +++ b/app/enums.py @@ -16,6 +16,41 @@ class NotificationStatus(StrEnum): VALIDATION_FAILED = "validation-failed" CANCELLED = "cancelled" + @classmethod + def sending_statuses(cls): + return [cls.CREATED, cls.PENDING, cls.SENDING] + + @classmethod + def delivered_statuses(cls): + return [cls.DELIVERED, cls.SENT] + + @classmethod + def failure_statuses(cls): + return [ + cls.FAILED, + cls.TEMPORARY_FAILURE, + cls.PERMANENT_FAILURE, + cls.TECHNICAL_FAILURE, + cls.VALIDATION_FAILED, + ] + + @classmethod + def requested_statuses(cls): + return ( + cls.sending_statuses() + cls.delivered_statuses() + cls.failure_statuses() + ) + + +class NotificationType(StrEnum): + EMAIL = "email" + SMS = "sms" + + +class OrganizationType(StrEnum): + FEDERAL = "federal" + STATE = "state" + OTHER = "other" + class ApiKeyType(StrEnum): NORMAL = "normal" @@ -53,17 +88,3 @@ class VerificationStatus(StrEnum): class AuthType(StrEnum): EMAIL_AUTH = "email_auth" SMS_AUTH = "sms_auth" - - -# TODO: -# class UserRole(StrEnum): -# ADMIN = "admin" -# USER = "user" -# GUEST = "guest" - - -# TODO: -# class NotificationType(StrEnum): -# EMAIL = "email" -# SMS = "sms" -# PUSH = "push" diff --git a/app/formatters.py b/app/formatters.py index a739fe437..590d36a02 100644 --- a/app/formatters.py +++ b/app/formatters.py @@ -17,6 +17,7 @@ from flask import render_template_string, url_for from flask.helpers import get_root_path from markupsafe import Markup +from app.enums import AuthType, NotificationStatus from app.utils.csv import get_user_preferred_timezone from app.utils.time import parse_naive_dt from notifications_utils.field import Field @@ -276,46 +277,53 @@ def format_notification_type(notification_type): def format_notification_status(status, template_type): return { "email": { - "failed": "Failed", - "technical-failure": "Technical failure", - "temporary-failure": "Inbox not accepting messages right now", - "permanent-failure": "Email address does not exist", - "delivered": "Delivered", - "sending": "Sending", - "created": "Sending", - "sent": "Delivered", + NotificationStatus.FAILED: "Failed", + NotificationStatus.TECHNICAL_FAILURE: "Technical failure", + NotificationStatus.TEMPORARY_FAILURE: "Inbox not accepting messages right now", + NotificationStatus.PERMANENT_FAILURE: "Email address does not exist", + NotificationStatus.DELIVERED: "Delivered", + NotificationStatus.SENDING: "Sending", + NotificationStatus.CREATED: "Sending", + NotificationStatus.SENT: "Delivered", }, "sms": { - "failed": "Failed", - "technical-failure": "Technical failure", - "temporary-failure": "Phone not accepting messages right now", - "permanent-failure": "Not delivered", - "delivered": "Delivered", - "sending": "Sending", - "created": "Sending", - "pending": "Sending", - "sent": "Sent", + NotificationStatus.FAILED: "Failed", + NotificationStatus.TECHNICAL_FAILURE: "Technical failure", + NotificationStatus.TEMPORARY_FAILURE: "Phone not accepting messages right now", + NotificationStatus.PERMANENT_FAILURE: "Not delivered", + NotificationStatus.DELIVERED: "Delivered", + NotificationStatus.SENDING: "Sending", + NotificationStatus.CREATED: "Sending", + NotificationStatus.PENDING: "Sending", + NotificationStatus.SENT: "Sent", }, }[template_type].get(status, status) def format_notification_status_as_time(status, created, updated): return dict.fromkeys( - {"created", "pending", "sending"}, " since {}".format(created) + { + NotificationStatus.CREATED, + NotificationStatus.PENDING, + NotificationStatus.SENDING, + }, + " since {}".format(created), ).get(status, updated) def format_notification_status_as_field_status(status, notification_type): return { - "failed": "error", - "technical-failure": "error", - "temporary-failure": "error", - "permanent-failure": "error", - "delivered": None, - "sent": "sent-international" if notification_type == "sms" else None, - "sending": "default", - "created": "default", - "pending": "default", + NotificationStatus.FAILED: "error", + NotificationStatus.TECHNICAL_FAILURE: "error", + NotificationStatus.TEMPORARY_FAILURE: "error", + NotificationStatus.PERMANENT_FAILURE: "error", + NotificationStatus.DELIVERED: None, + NotificationStatus.SENT: ( + "sent-international" if notification_type == "sms" else None + ), + NotificationStatus.SENDING: "default", + NotificationStatus.CREATED: "default", + NotificationStatus.PENDING: "default", }.get(status, "error") @@ -323,9 +331,9 @@ def format_notification_status_as_url(status, notification_type): url = partial(url_for, "main.message_status") if status not in { - "technical-failure", - "temporary-failure", - "permanent-failure", + NotificationStatus.TECHNICAL_FAILURE, + NotificationStatus.TEMPORARY_FAILURE, + NotificationStatus.PERMANENT_FAILURE, }: return None @@ -559,8 +567,8 @@ def square_metres_to_square_miles(area): def format_auth_type(auth_type, with_indefinite_article=False): indefinite_article, auth_type = { - "email_auth": ("an", "Email link"), - "sms_auth": ("a", "Text message code"), + AuthType.EMAIL_AUTH: ("an", "Email link"), + AuthType.SMS_AUTH: ("a", "Text message code"), }[auth_type] if with_indefinite_article: diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py index f6ec2fe50..507be6629 100644 --- a/app/main/views/add_service.py +++ b/app/main/views/add_service.py @@ -2,6 +2,7 @@ from flask import current_app, redirect, render_template, session, url_for from flask_login import current_user from app import service_api_client +from app.enums import OrganizationType from app.formatters import email_safe from app.main import main from app.main.forms import CreateServiceForm @@ -46,7 +47,7 @@ def _create_example_template(service_id): def add_service(): default_organization_type = current_user.default_organization_type if default_organization_type is None: - default_organization_type = "federal" + default_organization_type = OrganizationType.FEDERAL form = CreateServiceForm( # This value is currently not useful but if it is not set it will result in a bug organization_type=default_organization_type diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 3d8adde61..7ff4f8a7f 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -14,6 +14,7 @@ from app import ( service_api_client, template_statistics_client, ) +from app.enums import JobStatus, NotificationStatus from app.main import main from app.main.views.user_profile import set_timezone from app.statistics_utils import get_formatted_percentage @@ -42,7 +43,9 @@ def service_dashboard(service_id): job_response = job_api_client.get_jobs(service_id)["data"] service_data_retention_days = 7 - active_jobs = [job for job in job_response if job["job_status"] != "cancelled"] + active_jobs = [ + job for job in job_response if job["job_status"] != JobStatus.CANCELLED + ] job_lists = [ {**job_dict, "finished_processing": job_is_finished(job_dict)} for job_dict in active_jobs @@ -69,7 +72,9 @@ def service_dashboard(service_id): def job_is_finished(job_dict): - done_statuses = DELIVERED_STATUSES + FAILURE_STATUSES + ["cancelled"] + done_statuses = ( + DELIVERED_STATUSES + FAILURE_STATUSES + [NotificationStatus.CANCELLED] + ) processed_count = sum( stat["count"] for stat in job_dict["statistics"] @@ -106,8 +111,18 @@ def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days): ] aggregator = { d: { - "sms": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0}, - "email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0}, + "sms": { + NotificationStatus.DELIVERED: 0, + "failure": 0, + NotificationStatus.PENDING: 0, + "requested": 0, + }, + "email": { + NotificationStatus.DELIVERED: 0, + "failure": 0, + NotificationStatus.PENDING: 0, + "requested": 0, + }, } for d in days_list } @@ -121,7 +136,12 @@ def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days): if local_day in aggregator: for msg_type in ["sms", "email"]: - for status in ["delivered", "failure", "pending", "requested"]: + for status in [ + NotificationStatus.DELIVERED, + "failure", + NotificationStatus.PENDING, + "requested", + ]: aggregator[local_day][msg_type][status] += data[msg_type][status] return aggregator @@ -231,7 +251,9 @@ def usage(service_id): def filter_out_cancelled_stats(template_statistics): - return [s for s in template_statistics if s["status"] != "cancelled"] + return [ + s for s in template_statistics if s["status"] != NotificationStatus.CANCELLED + ] def aggregate_template_usage(template_statistics, sort_key="count"): @@ -265,7 +287,7 @@ def get_dashboard_totals(statistics): for msg_type in statistics.values(): msg_type["failed_percentage"] = get_formatted_percentage( - msg_type["failed"], msg_type["requested"] + msg_type[NotificationStatus.FAILED], msg_type["requested"] ) msg_type["show_warning"] = float(msg_type["failed_percentage"]) > 3 @@ -314,7 +336,9 @@ def aggregate_status_types(counts_dict): return get_dashboard_totals( { "{}_counts".format(message_type): { - "failed": sum(stats.get(status, 0) for status in FAILURE_STATUSES), + NotificationStatus.FAILED: sum( + stats.get(status, 0) for status in FAILURE_STATUSES + ), "requested": sum(stats.get(status, 0) for status in REQUESTED_STATUSES), } for message_type, stats in counts_dict.items() diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index d9e282122..189b84562 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -23,7 +23,7 @@ from app import ( notification_api_client, service_api_client, ) -from app.enums import JobStatus +from app.enums import JobStatus, NotificationStatus from app.formatters import get_time_left, message_count_noun from app.main import main from app.main.forms import SearchNotificationsForm @@ -304,22 +304,30 @@ def get_status_filters(service, message_type, statistics): if message_type is None: stats = { key: sum(statistics[message_type][key] for message_type in {"email", "sms"}) - for key in {"requested", "delivered", "failed"} + for key in { + "requested", + NotificationStatus.DELIVERED, + NotificationStatus.FAILED, + } } else: stats = statistics[message_type] if stats.get("failure") is not None: - stats["failed"] = stats["failure"] + stats[NotificationStatus.FAILED] = stats["failure"] - stats["pending"] = stats["requested"] - stats["delivered"] - stats["failed"] + stats[NotificationStatus.PENDING] = ( + stats["requested"] + - stats[NotificationStatus.DELIVERED] + - stats[NotificationStatus.FAILED] + ) filters = [ # key, label, option ("requested", "total", "sending,delivered,failed"), - ("pending", "pending", "sending,pending"), - ("delivered", "delivered", "delivered"), - ("failed", "failed", "failed"), + (NotificationStatus.PENDING, "pending", "sending,pending"), + (NotificationStatus.DELIVERED, "delivered", "delivered"), + (NotificationStatus.FAILED, "failed", "failed"), ] return [ # return list containing label, option, link, count diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index fb9ea7fbe..478f411dc 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -25,6 +25,7 @@ from app import ( service_api_client, user_api_client, ) +from app.enums import NotificationStatus from app.extensions import redis_client from app.main import main from app.main.forms import ( @@ -769,32 +770,41 @@ def filter_and_sort_services(services, trial_mode_services=False): def create_global_stats(services): stats = { - "email": {"delivered": 0, "failed": 0, "requested": 0}, - "sms": {"delivered": 0, "failed": 0, "requested": 0}, + "email": { + NotificationStatus.DELIVERED: 0, + NotificationStatus.FAILED: 0, + "requested": 0, + }, + "sms": { + NotificationStatus.DELIVERED: 0, + NotificationStatus.FAILED: 0, + "requested": 0, + }, } # Issue #1323. The back end is now sending 'failure' instead of # 'failed'. Adjust it here, but keep it flexible in case # the backend reverts to 'failed'. for service in services: if service["statistics"]["sms"].get("failure") is not None: - service["statistics"]["sms"]["failed"] = service["statistics"]["sms"][ - "failure" - ] + service["statistics"]["sms"][NotificationStatus.FAILED] = service[ + "statistics" + ]["sms"]["failure"] if service["statistics"]["email"].get("failure") is not None: - service["statistics"]["email"]["failed"] = service["statistics"]["email"][ - "failure" - ] + service["statistics"]["email"][NotificationStatus.FAILED] = service[ + "statistics" + ]["email"]["failure"] for service in services: for msg_type, status in itertools.product( - ("sms", "email"), ("delivered", "failed", "requested") + ("sms", "email"), + (NotificationStatus.DELIVERED, NotificationStatus.FAILED, "requested"), ): stats[msg_type][status] += service["statistics"][msg_type][status] for stat in stats.values(): stat["failure_rate"] = get_formatted_percentage( - stat["failed"], stat["requested"] + stat[NotificationStatus.FAILED], stat["requested"] ) return stats diff --git a/app/models/organization.py b/app/models/organization.py index e9e30f460..f2dc1bf07 100644 --- a/app/models/organization.py +++ b/app/models/organization.py @@ -2,14 +2,15 @@ from collections import OrderedDict from werkzeug.utils import cached_property +from app.enums import OrganizationType from app.models import JSONModel, ModelList, SerialisedModelCollection, SortByNameMixin from app.notify_client.organizations_api_client import organizations_client class Organization(JSONModel, SortByNameMixin): - TYPE_FEDERAL = "federal" - TYPE_STATE = "state" - TYPE_OTHER = "other" + TYPE_FEDERAL = OrganizationType.FEDERAL + TYPE_STATE = OrganizationType.STATE + TYPE_OTHER = OrganizationType.OTHER TYPE_LABELS = OrderedDict( [ diff --git a/app/models/service.py b/app/models/service.py index e9bcf8a7d..c2f29a8fe 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -322,7 +322,7 @@ class Service(JSONModel, SortByNameMixin): @property def shouldnt_use_govuk_as_sms_sender(self): - return self.organization_type != Organization.TYPE_CENTRAL + return self.organization_type != Organization.TYPE_FEDERAL @cached_property def sms_senders(self): diff --git a/app/utils/__init__.py b/app/utils/__init__.py index b9aaaaa88..842b63bdf 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -7,25 +7,15 @@ from ordered_set import OrderedSet from werkzeug.datastructures import MultiDict from werkzeug.routing import RequestRedirect -from app.enums import NotificationStatus +from app.enums import NotificationStatus, NotificationType from notifications_utils.field import Field -SENDING_STATUSES = [ - NotificationStatus.CREATED, - NotificationStatus.PENDING, - NotificationStatus.SENDING, -] -DELIVERED_STATUSES = [NotificationStatus.DELIVERED, NotificationStatus.SENT] -FAILURE_STATUSES = [ - NotificationStatus.FAILED, - NotificationStatus.TEMPORARY_FAILURE, - NotificationStatus.PERMANENT_FAILURE, - NotificationStatus.TECHNICAL_FAILURE, - NotificationStatus.VALIDATION_FAILED, -] -REQUESTED_STATUSES = SENDING_STATUSES + DELIVERED_STATUSES + FAILURE_STATUSES +SENDING_STATUSES = NotificationStatus.sending_statuses() +DELIVERED_STATUSES = NotificationStatus.delivered_statuses() +FAILURE_STATUSES = NotificationStatus.failure_statuses() +REQUESTED_STATUSES = NotificationStatus.requested_statuses() -NOTIFICATION_TYPES = ["sms", "email"] +NOTIFICATION_TYPES = [NotificationType.SMS, NotificationType.EMAIL] def service_has_permission(permission):