2025-05-16 14:14:50 -04:00
|
|
|
from datetime import datetime, timezone
|
2025-05-14 15:01:01 -04:00
|
|
|
from uuid import UUID
|
2025-05-09 15:14:07 -04:00
|
|
|
|
2025-06-02 10:44:42 -04:00
|
|
|
from flask import current_app
|
2025-05-16 14:14:50 -04:00
|
|
|
from marshmallow import EXCLUDE, Schema, fields, post_dump
|
2025-05-09 15:14:07 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicTemplateSchema(Schema):
|
|
|
|
|
id = fields.UUID(required=True)
|
|
|
|
|
name = fields.String(required=True)
|
|
|
|
|
template_type = fields.String(required=True)
|
|
|
|
|
version = fields.Integer(required=True)
|
2025-05-16 16:23:59 -04:00
|
|
|
content = fields.String(allow_none=True) # for fallback rendering
|
2025-05-09 15:14:07 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicJobSchema(Schema):
|
|
|
|
|
id = fields.UUID(required=True)
|
|
|
|
|
original_file_name = fields.String(required=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicNotificationSchema(Schema):
|
|
|
|
|
id = fields.UUID(required=True)
|
|
|
|
|
to = fields.String(required=True)
|
|
|
|
|
job_row_number = fields.Integer(allow_none=True)
|
|
|
|
|
template_version = fields.Integer(required=True)
|
|
|
|
|
billable_units = fields.Integer(required=True)
|
|
|
|
|
notification_type = fields.String(required=True)
|
|
|
|
|
created_at = fields.String(required=True)
|
|
|
|
|
sent_at = fields.String(allow_none=True)
|
|
|
|
|
updated_at = fields.String(allow_none=True)
|
|
|
|
|
sent_by = fields.String(allow_none=True)
|
|
|
|
|
status = fields.String(required=True)
|
|
|
|
|
reference = fields.String(allow_none=True)
|
|
|
|
|
template = fields.Nested(PublicTemplateSchema, required=True)
|
2025-05-16 16:23:59 -04:00
|
|
|
service = fields.Raw(required=True)
|
2025-05-09 15:14:07 -04:00
|
|
|
job = fields.Nested(PublicJobSchema, allow_none=True)
|
2025-05-16 16:23:59 -04:00
|
|
|
api_key = fields.Raw(allow_none=True)
|
2025-05-09 15:14:07 -04:00
|
|
|
body = fields.String(required=True)
|
2025-05-16 16:23:59 -04:00
|
|
|
content_char_count = fields.Integer(allow_none=True)
|
2025-05-14 15:01:01 -04:00
|
|
|
|
|
|
|
|
@post_dump
|
2025-05-16 16:23:59 -04:00
|
|
|
def transform_common_fields(self, data, **kwargs):
|
2025-05-09 15:14:07 -04:00
|
|
|
def to_rfc3339(dt):
|
|
|
|
|
if dt is None:
|
|
|
|
|
return None
|
2025-05-14 15:01:01 -04:00
|
|
|
if isinstance(dt, str):
|
|
|
|
|
try:
|
|
|
|
|
dt = datetime.fromisoformat(dt)
|
|
|
|
|
except ValueError:
|
2025-05-16 16:23:59 -04:00
|
|
|
return dt
|
2025-05-09 15:14:07 -04:00
|
|
|
if dt.tzinfo is None:
|
|
|
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
|
|
|
return dt.isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
2025-05-16 16:23:59 -04:00
|
|
|
def normalize_uuid(val):
|
|
|
|
|
if hasattr(val, "id"):
|
|
|
|
|
return str(val.id)
|
|
|
|
|
elif isinstance(val, UUID):
|
|
|
|
|
return str(val)
|
|
|
|
|
elif isinstance(val, str):
|
|
|
|
|
if val.startswith("Service "):
|
|
|
|
|
return val.replace("Service ", "").strip()
|
|
|
|
|
elif val.startswith("ApiKey "):
|
|
|
|
|
return val.replace("ApiKey ", "").strip()
|
|
|
|
|
return val
|
|
|
|
|
elif hasattr(val, "__str__") and "Service " in str(val):
|
|
|
|
|
return str(val).replace("Service ", "").strip()
|
|
|
|
|
return str(val) if val else None
|
|
|
|
|
|
2025-05-14 15:01:01 -04:00
|
|
|
data["created_at"] = to_rfc3339(data.get("created_at"))
|
|
|
|
|
data["sent_at"] = to_rfc3339(data.get("sent_at"))
|
|
|
|
|
data["updated_at"] = to_rfc3339(data.get("updated_at"))
|
|
|
|
|
|
2025-05-16 16:23:59 -04:00
|
|
|
data["service"] = normalize_uuid(data.get("service"))
|
|
|
|
|
data["api_key"] = normalize_uuid(data.get("api_key"))
|
|
|
|
|
|
|
|
|
|
if "job" in data and isinstance(data["job"], dict) and "id" in data["job"]:
|
|
|
|
|
data["job"]["id"] = normalize_uuid(data["job"]["id"])
|
|
|
|
|
|
|
|
|
|
if "body" not in data or not data["body"]:
|
|
|
|
|
data["body"] = data.get("template", {}).get("content") or ""
|
|
|
|
|
|
|
|
|
|
notification = getattr(self, "context", {}).get("notification_instance")
|
|
|
|
|
if "content_char_count" not in data:
|
|
|
|
|
if (
|
|
|
|
|
notification
|
|
|
|
|
and getattr(notification, "content_char_count", None) is not None
|
|
|
|
|
):
|
|
|
|
|
data["content_char_count"] = notification.content_char_count
|
|
|
|
|
elif (
|
|
|
|
|
notification
|
|
|
|
|
and notification.template
|
|
|
|
|
and notification.template.template_type == "email"
|
|
|
|
|
):
|
|
|
|
|
# this is expected to make the test pass, but I suspect the test might be wrong and should have a count
|
|
|
|
|
data["content_char_count"] = None
|
|
|
|
|
elif data.get("body") is not None:
|
|
|
|
|
data["content_char_count"] = len(data["body"])
|
|
|
|
|
else:
|
|
|
|
|
data["content_char_count"] = None
|
|
|
|
|
|
|
|
|
|
if "template" in data:
|
|
|
|
|
data["template"].pop("content", None)
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicNotificationResponseSchema(PublicNotificationSchema):
|
|
|
|
|
class Meta:
|
|
|
|
|
unknown = EXCLUDE
|
|
|
|
|
|
|
|
|
|
@post_dump
|
|
|
|
|
def transform_subject(self, data, **kwargs):
|
|
|
|
|
notification = getattr(self, "context", {}).get("notification_instance")
|
|
|
|
|
subject = getattr(self, "context", {}).get("template_subject")
|
|
|
|
|
|
|
|
|
|
template_type = data.get("template", {}).get("template_type")
|
|
|
|
|
if template_type != "email":
|
|
|
|
|
data.pop("subject", None)
|
|
|
|
|
elif "subject" not in data:
|
|
|
|
|
if subject:
|
|
|
|
|
data["subject"] = subject
|
|
|
|
|
elif notification and hasattr(notification, "subject"):
|
|
|
|
|
try:
|
|
|
|
|
data["subject"] = str(notification.subject)
|
2025-06-02 10:44:42 -04:00
|
|
|
except AttributeError:
|
|
|
|
|
data["subject"] = ""
|
|
|
|
|
current_app.logger.debug("Notification has no subject attribute")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
data["subject"] = ""
|
|
|
|
|
current_app.logger.warning(
|
|
|
|
|
f"Error getting notification subject: {e}"
|
|
|
|
|
)
|
2025-05-14 15:01:01 -04:00
|
|
|
|
|
|
|
|
return data
|