This commit is contained in:
Kenneth Kehl
2023-08-29 14:54:30 -07:00
parent 19dcd7a48b
commit 1ecb747c6d
588 changed files with 34100 additions and 23589 deletions

View File

@@ -25,12 +25,14 @@ from app.service.service_callback_api_schema import (
update_service_callback_api_schema,
)
service_callback_blueprint = Blueprint('service_callback', __name__, url_prefix='/service/<uuid:service_id>')
service_callback_blueprint = Blueprint(
"service_callback", __name__, url_prefix="/service/<uuid:service_id>"
)
register_errors(service_callback_blueprint)
@service_callback_blueprint.route('/inbound-api', methods=['POST'])
@service_callback_blueprint.route("/inbound-api", methods=["POST"])
def create_service_inbound_api(service_id):
data = request.get_json()
validate(data, create_service_callback_api_schema)
@@ -39,45 +41,51 @@ def create_service_inbound_api(service_id):
try:
save_service_inbound_api(inbound_api)
except SQLAlchemyError as e:
return handle_sql_error(e, 'service_inbound_api')
return handle_sql_error(e, "service_inbound_api")
return jsonify(data=inbound_api.serialize()), 201
@service_callback_blueprint.route('/inbound-api/<uuid:inbound_api_id>', methods=['POST'])
@service_callback_blueprint.route(
"/inbound-api/<uuid:inbound_api_id>", methods=["POST"]
)
def update_service_inbound_api(service_id, inbound_api_id):
data = request.get_json()
validate(data, update_service_callback_api_schema)
to_update = get_service_inbound_api(inbound_api_id, service_id)
reset_service_inbound_api(service_inbound_api=to_update,
updated_by_id=data["updated_by_id"],
url=data.get("url", None),
bearer_token=data.get("bearer_token", None))
reset_service_inbound_api(
service_inbound_api=to_update,
updated_by_id=data["updated_by_id"],
url=data.get("url", None),
bearer_token=data.get("bearer_token", None),
)
return jsonify(data=to_update.serialize()), 200
@service_callback_blueprint.route('/inbound-api/<uuid:inbound_api_id>', methods=['GET'])
@service_callback_blueprint.route("/inbound-api/<uuid:inbound_api_id>", methods=["GET"])
def fetch_service_inbound_api(service_id, inbound_api_id):
inbound_api = get_service_inbound_api(inbound_api_id, service_id)
return jsonify(data=inbound_api.serialize()), 200
@service_callback_blueprint.route('/inbound-api/<uuid:inbound_api_id>', methods=['DELETE'])
@service_callback_blueprint.route(
"/inbound-api/<uuid:inbound_api_id>", methods=["DELETE"]
)
def remove_service_inbound_api(service_id, inbound_api_id):
inbound_api = get_service_inbound_api(inbound_api_id, service_id)
if not inbound_api:
error = 'Service inbound API not found'
error = "Service inbound API not found"
raise InvalidRequest(error, status_code=404)
delete_service_inbound_api(inbound_api)
return '', 204
return "", 204
@service_callback_blueprint.route('/delivery-receipt-api', methods=['POST'])
@service_callback_blueprint.route("/delivery-receipt-api", methods=["POST"])
def create_service_callback_api(service_id):
data = request.get_json()
validate(data, create_service_callback_api_schema)
@@ -87,56 +95,85 @@ def create_service_callback_api(service_id):
try:
save_service_callback_api(callback_api)
except SQLAlchemyError as e:
return handle_sql_error(e, 'service_callback_api')
return handle_sql_error(e, "service_callback_api")
return jsonify(data=callback_api.serialize()), 201
@service_callback_blueprint.route('/delivery-receipt-api/<uuid:callback_api_id>', methods=['POST'])
@service_callback_blueprint.route(
"/delivery-receipt-api/<uuid:callback_api_id>", methods=["POST"]
)
def update_service_callback_api(service_id, callback_api_id):
data = request.get_json()
validate(data, update_service_callback_api_schema)
to_update = get_service_callback_api(callback_api_id, service_id)
reset_service_callback_api(service_callback_api=to_update,
updated_by_id=data["updated_by_id"],
url=data.get("url", None),
bearer_token=data.get("bearer_token", None))
reset_service_callback_api(
service_callback_api=to_update,
updated_by_id=data["updated_by_id"],
url=data.get("url", None),
bearer_token=data.get("bearer_token", None),
)
return jsonify(data=to_update.serialize()), 200
@service_callback_blueprint.route('/delivery-receipt-api/<uuid:callback_api_id>', methods=["GET"])
@service_callback_blueprint.route(
"/delivery-receipt-api/<uuid:callback_api_id>", methods=["GET"]
)
def fetch_service_callback_api(service_id, callback_api_id):
callback_api = get_service_callback_api(callback_api_id, service_id)
return jsonify(data=callback_api.serialize()), 200
@service_callback_blueprint.route('/delivery-receipt-api/<uuid:callback_api_id>', methods=['DELETE'])
@service_callback_blueprint.route(
"/delivery-receipt-api/<uuid:callback_api_id>", methods=["DELETE"]
)
def remove_service_callback_api(service_id, callback_api_id):
callback_api = get_service_callback_api(callback_api_id, service_id)
if not callback_api:
error = 'Service delivery receipt callback API not found'
error = "Service delivery receipt callback API not found"
raise InvalidRequest(error, status_code=404)
delete_service_callback_api(callback_api)
return '', 204
return "", 204
def handle_sql_error(e, table_name):
if hasattr(e, 'orig') and hasattr(e.orig, 'pgerror') and e.orig.pgerror \
and ('duplicate key value violates unique constraint "ix_{}_service_id"'.format(table_name)
in e.orig.pgerror):
return jsonify(
result='error',
message={'name': ["You can only have one URL and bearer token for your service."]}
), 400
elif hasattr(e, 'orig') and hasattr(e.orig, 'pgerror') and e.orig.pgerror \
and ('insert or update on table "{0}" violates '
'foreign key constraint "{0}_service_id_fkey"'.format(table_name)
in e.orig.pgerror):
return jsonify(result='error', message="No result found"), 404
if (
hasattr(e, "orig")
and hasattr(e.orig, "pgerror")
and e.orig.pgerror
and (
'duplicate key value violates unique constraint "ix_{}_service_id"'.format(
table_name
)
in e.orig.pgerror
)
):
return (
jsonify(
result="error",
message={
"name": [
"You can only have one URL and bearer token for your service."
]
},
),
400,
)
elif (
hasattr(e, "orig")
and hasattr(e.orig, "pgerror")
and e.orig.pgerror
and (
'insert or update on table "{0}" violates '
'foreign key constraint "{0}_service_id_fkey"'.format(table_name)
in e.orig.pgerror
)
):
return jsonify(result="error", message="No result found"), 404
else:
raise e

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,10 @@ from app.v2.errors import BadRequestError
def validate_created_by(service, created_by_id):
user = get_user_by_id(created_by_id)
if service not in user.services:
message = 'Cant create notification - {} is not part of the "{}" service'.format(
user.name,
service.name
message = (
'Cant create notification - {} is not part of the "{}" service'.format(
user.name, service.name
)
)
raise BadRequestError(message=message)
@@ -36,16 +37,15 @@ def create_one_off_reference(template_type):
def send_one_off_notification(service_id, post_data):
service = dao_fetch_service_by_id(service_id)
template = dao_get_template_by_id_and_service_id(
template_id=post_data['template_id'],
service_id=service_id
template_id=post_data["template_id"], service_id=service_id
)
personalisation = post_data.get('personalisation', None)
personalisation = post_data.get("personalisation", None)
validate_template(template.id, personalisation, service, template.template_type)
validate_and_format_recipient(
send_to=post_data['to'],
send_to=post_data["to"],
key_type=KEY_TYPE_NORMAL,
service=service,
notification_type=template.template_type,
@@ -53,28 +53,28 @@ def send_one_off_notification(service_id, post_data):
)
client_reference = None
validate_created_by(service, post_data['created_by'])
validate_created_by(service, post_data["created_by"])
sender_id = post_data.get('sender_id', None)
sender_id = post_data.get("sender_id", None)
reply_to = get_reply_to_text(
notification_type=template.template_type,
sender_id=sender_id,
service=service,
template=template
template=template,
)
notification = persist_notification(
template_id=template.id,
template_version=template.version,
recipient=post_data['to'],
recipient=post_data["to"],
service=service,
personalisation=personalisation,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL,
created_by_id=post_data['created_by'],
created_by_id=post_data["created_by"],
reply_to_text=reply_to,
reference=create_one_off_reference(template.template_type),
client_reference=client_reference
client_reference=client_reference,
)
queue_name = QueueNames.PRIORITY if template.process_type == PRIORITY else None
@@ -84,7 +84,7 @@ def send_one_off_notification(service_id, post_data):
queue=queue_name,
)
return {'id': str(notification.id)}
return {"id": str(notification.id)}
def get_reply_to_text(notification_type, sender_id, service, template):
@@ -92,11 +92,13 @@ def get_reply_to_text(notification_type, sender_id, service, template):
if sender_id:
try:
if notification_type == EMAIL_TYPE:
message = 'Reply to email address not found'
message = "Reply to email address not found"
reply_to = dao_get_reply_to_by_id(service.id, sender_id).email_address
elif notification_type == SMS_TYPE:
message = 'SMS sender not found'
reply_to = dao_get_service_sms_senders_by_id(service.id, sender_id).get_reply_to_text()
message = "SMS sender not found"
reply_to = dao_get_service_sms_senders_by_id(
service.id, sender_id
).get_reply_to_text()
except NoResultFound:
raise BadRequestError(message=message)
else:

View File

@@ -13,26 +13,30 @@ from app.notifications.process_notifications import (
)
def send_notification_to_service_users(service_id, template_id, personalisation=None, include_user_fields=None):
def send_notification_to_service_users(
service_id, template_id, personalisation=None, include_user_fields=None
):
personalisation = personalisation or {}
include_user_fields = include_user_fields or []
template = dao_get_template_by_id(template_id)
service = dao_fetch_service_by_id(service_id)
active_users = dao_fetch_active_users_for_service(service.id)
notify_service = dao_fetch_service_by_id(current_app.config['NOTIFY_SERVICE_ID'])
notify_service = dao_fetch_service_by_id(current_app.config["NOTIFY_SERVICE_ID"])
for user in active_users:
personalisation = _add_user_fields(user, personalisation, include_user_fields)
notification = persist_notification(
template_id=template.id,
template_version=template.version,
recipient=user.email_address if template.template_type == EMAIL_TYPE else user.mobile_number,
recipient=user.email_address
if template.template_type == EMAIL_TYPE
else user.mobile_number,
service=notify_service,
personalisation=personalisation,
notification_type=template.template_type,
api_key_id=None,
key_type=KEY_TYPE_NORMAL,
reply_to_text=notify_service.get_default_reply_to_email_address()
reply_to_text=notify_service.get_default_reply_to_email_address(),
)
send_notification_to_queue(notification, queue=QueueNames.NOTIFY)

View File

@@ -8,9 +8,9 @@ create_service_callback_api_schema = {
"properties": {
"url": https_url,
"bearer_token": {"type": "string", "minLength": 10},
"updated_by_id": uuid
"updated_by_id": uuid,
},
"required": ["url", "bearer_token", "updated_by_id"]
"required": ["url", "bearer_token", "updated_by_id"],
}
update_service_callback_api_schema = {
@@ -21,7 +21,7 @@ update_service_callback_api_schema = {
"properties": {
"url": https_url,
"bearer_token": {"type": "string", "minLength": 10},
"updated_by_id": uuid
"updated_by_id": uuid,
},
"required": ["updated_by_id"]
"required": ["updated_by_id"],
}

View File

@@ -7,7 +7,7 @@ add_service_data_retention_request = {
"days_of_retention": {"type": "integer"},
"notification_type": {"enum": ["sms", "email"]},
},
"required": ["days_of_retention", "notification_type"]
"required": ["days_of_retention", "notification_type"],
}
@@ -19,5 +19,5 @@ update_service_data_retention_request = {
"properties": {
"days_of_retention": {"type": "integer"},
},
"required": ["days_of_retention"]
"required": ["days_of_retention"],
}

View File

@@ -7,9 +7,9 @@ add_service_email_reply_to_request = {
"title": "Add new email reply to address for service",
"properties": {
"email_address": {"type": "string", "format": "email_address"},
"is_default": {"type": "boolean"}
"is_default": {"type": "boolean"},
},
"required": ["email_address", "is_default"]
"required": ["email_address", "is_default"],
}
@@ -21,7 +21,7 @@ add_service_sms_sender_request = {
"properties": {
"sms_sender": {"type": "string"},
"is_default": {"type": "boolean"},
"inbound_number_id": uuid
"inbound_number_id": uuid,
},
"required": ["sms_sender", "is_default"]
"required": ["sms_sender", "is_default"],
}

View File

@@ -23,12 +23,17 @@ def format_admin_stats(statistics):
counts = create_stats_dict()
for row in statistics:
if row.key_type == 'test':
counts[row.notification_type]['test-key'] += row.count
if row.key_type == "test":
counts[row.notification_type]["test-key"] += row.count
else:
counts[row.notification_type]['total'] += row.count
if row.status in ('technical-failure', 'permanent-failure', 'temporary-failure', 'virus-scan-failed'):
counts[row.notification_type]['failures'][row.status] += row.count
counts[row.notification_type]["total"] += row.count
if row.status in (
"technical-failure",
"permanent-failure",
"temporary-failure",
"virus-scan-failed",
):
counts[row.notification_type]["failures"][row.status] += row.count
return counts
@@ -38,35 +43,32 @@ def create_stats_dict():
for template in NOTIFICATION_TYPES:
stats_dict[template] = {}
for status in ('total', 'test-key'):
for status in ("total", "test-key"):
stats_dict[template][status] = 0
stats_dict[template]['failures'] = {
'technical-failure': 0,
'permanent-failure': 0,
'temporary-failure': 0,
'virus-scan-failed': 0,
stats_dict[template]["failures"] = {
"technical-failure": 0,
"permanent-failure": 0,
"temporary-failure": 0,
"virus-scan-failed": 0,
}
return stats_dict
def format_monthly_template_notification_stats(year, rows):
stats = {
datetime.strftime(date, '%Y-%m'): {}
for date in [
datetime(year, month, 1) for month in range(4, 13)
] + [
datetime(year + 1, month, 1) for month in range(1, 4)
]
datetime.strftime(date, "%Y-%m"): {}
for date in [datetime(year, month, 1) for month in range(4, 13)]
+ [datetime(year + 1, month, 1) for month in range(1, 4)]
}
for row in rows:
formatted_month = row.month.strftime('%Y-%m')
formatted_month = row.month.strftime("%Y-%m")
if str(row.template_id) not in stats[formatted_month]:
stats[formatted_month][str(row.template_id)] = {
"name": row.name,
"type": row.template_type,
"counts": dict.fromkeys(NOTIFICATION_STATUS_TYPES, 0)
"counts": dict.fromkeys(NOTIFICATION_STATUS_TYPES, 0),
}
stats[formatted_month][str(row.template_id)]["counts"][row.status] += row.count
@@ -75,30 +77,33 @@ def format_monthly_template_notification_stats(year, rows):
def create_zeroed_stats_dicts():
return {
template_type: {
status: 0 for status in ('requested', 'delivered', 'failed')
} for template_type in NOTIFICATION_TYPES
template_type: {status: 0 for status in ("requested", "delivered", "failed")}
for template_type in NOTIFICATION_TYPES
}
def _update_statuses_from_row(update_dict, row):
if row.status != 'cancelled':
update_dict['requested'] += row.count
if row.status in ('delivered', 'sent'):
update_dict['delivered'] += row.count
if row.status != "cancelled":
update_dict["requested"] += row.count
if row.status in ("delivered", "sent"):
update_dict["delivered"] += row.count
elif row.status in (
'failed', 'technical-failure', 'temporary-failure',
'permanent-failure', 'validation-failed', 'virus-scan-failed'):
update_dict['failed'] += row.count
"failed",
"technical-failure",
"temporary-failure",
"permanent-failure",
"validation-failed",
"virus-scan-failed",
):
update_dict["failed"] += row.count
def create_empty_monthly_notification_status_stats_dict(year):
utc_month_starts = get_months_for_financial_year(year)
# nested dicts - data[month][template type][status] = count
return {
start.strftime('%Y-%m'): {
template_type: defaultdict(int)
for template_type in NOTIFICATION_TYPES
start.strftime("%Y-%m"): {
template_type: defaultdict(int) for template_type in NOTIFICATION_TYPES
}
for start in utc_month_starts
}
@@ -106,7 +111,7 @@ 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')
month = row.month.strftime("%Y-%m")
data[month][row.notification_type][row.notification_status] += row.count

View File

@@ -21,17 +21,15 @@ def get_guest_list_objects(service_id, request_json):
return [
ServiceGuestList.from_string(service_id, type, recipient)
for type, recipient in (
get_recipients_from_request(request_json,
'phone_numbers',
MOBILE_TYPE) +
get_recipients_from_request(request_json,
'email_addresses',
EMAIL_TYPE)
get_recipients_from_request(request_json, "phone_numbers", MOBILE_TYPE)
+ get_recipients_from_request(request_json, "email_addresses", EMAIL_TYPE)
)
]
def service_allowed_to_send_to(recipient, service, key_type, allow_guest_list_recipients=True):
def service_allowed_to_send_to(
recipient, service, key_type, allow_guest_list_recipients=True
):
if key_type == KEY_TYPE_TEST:
return True
@@ -46,18 +44,12 @@ def service_allowed_to_send_to(recipient, service, key_type, allow_guest_list_re
[user.mobile_number, user.email_address] for user in service.users
)
guest_list_members = [
member.recipient for member in service.guest_list
if allow_guest_list_recipients
member.recipient for member in service.guest_list if allow_guest_list_recipients
]
if (
(key_type == KEY_TYPE_NORMAL and service.restricted) or
(key_type == KEY_TYPE_TEAM)
if (key_type == KEY_TYPE_NORMAL and service.restricted) or (
key_type == KEY_TYPE_TEAM
):
return allowed_to_send_to(
recipient,
itertools.chain(
team_members,
guest_list_members
)
recipient, itertools.chain(team_members, guest_list_members)
)