2023-12-22 10:21:42 -07:00
|
|
|
|
import os
|
2021-01-06 12:12:01 +00:00
|
|
|
|
import re
|
|
|
|
|
|
import unicodedata
|
|
|
|
|
|
import urllib
|
|
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
|
|
from functools import partial
|
|
|
|
|
|
from math import floor, log10
|
|
|
|
|
|
from numbers import Number
|
|
|
|
|
|
|
|
|
|
|
|
import ago
|
|
|
|
|
|
import dateutil
|
|
|
|
|
|
import humanize
|
2023-12-22 10:21:42 -07:00
|
|
|
|
import markdown
|
2023-06-12 17:00:20 -04:00
|
|
|
|
import pytz
|
2024-01-03 22:01:07 -07:00
|
|
|
|
from bs4 import BeautifulSoup
|
2024-04-04 18:09:22 -07:00
|
|
|
|
from flask import render_template_string, url_for
|
2023-12-22 10:03:43 -07:00
|
|
|
|
from flask.helpers import get_root_path
|
2024-04-04 18:09:22 -07:00
|
|
|
|
from markupsafe import Markup
|
2024-05-16 10:37:37 -04:00
|
|
|
|
|
|
|
|
|
|
from app.utils.csv import get_user_preferred_timezone
|
|
|
|
|
|
from app.utils.time import parse_naive_dt
|
2021-01-06 12:12:01 +00:00
|
|
|
|
from notifications_utils.field import Field
|
|
|
|
|
|
from notifications_utils.formatters import make_quotes_smart
|
|
|
|
|
|
from notifications_utils.formatters import nl2br as utils_nl2br
|
2023-08-25 08:57:24 -07:00
|
|
|
|
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
|
2021-01-06 12:12:01 +00:00
|
|
|
|
from notifications_utils.take import Take
|
|
|
|
|
|
|
2022-11-28 20:48:25 -05:00
|
|
|
|
|
2024-01-03 22:01:07 -07:00
|
|
|
|
def apply_html_class(tags, html_file):
|
|
|
|
|
|
new_html = html_file
|
|
|
|
|
|
|
|
|
|
|
|
for tag in tags:
|
|
|
|
|
|
element = tag[0]
|
|
|
|
|
|
class_name = tag[1]
|
|
|
|
|
|
|
2024-01-11 12:43:49 -05:00
|
|
|
|
soup = BeautifulSoup(new_html, "html.parser")
|
2024-01-03 22:01:07 -07:00
|
|
|
|
|
|
|
|
|
|
for xtag in soup.find_all(element):
|
2024-01-11 12:43:49 -05:00
|
|
|
|
xtag["class"] = class_name
|
2024-01-03 22:01:07 -07:00
|
|
|
|
|
|
|
|
|
|
new_html = str(soup)
|
|
|
|
|
|
|
|
|
|
|
|
return new_html
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-12-26 22:04:06 -07:00
|
|
|
|
def convert_markdown_template(mdf, test=False):
|
2023-12-27 12:59:45 -07:00
|
|
|
|
content_text = ""
|
|
|
|
|
|
|
2023-12-26 22:04:06 -07:00
|
|
|
|
if not test:
|
2024-01-11 12:43:49 -05:00
|
|
|
|
APP_ROOT = get_root_path("notifications-admin")
|
|
|
|
|
|
file = "app/content/" + mdf + ".md"
|
2023-12-26 22:04:06 -07:00
|
|
|
|
md_file = os.path.join(APP_ROOT, file)
|
|
|
|
|
|
with open(md_file) as f:
|
|
|
|
|
|
content_text = f.read()
|
|
|
|
|
|
else:
|
|
|
|
|
|
content_text = mdf
|
2023-12-22 10:03:43 -07:00
|
|
|
|
|
2024-01-03 22:01:07 -07:00
|
|
|
|
jn_render = render_template_string(content_text)
|
|
|
|
|
|
md_render = markdown.markdown(jn_render)
|
|
|
|
|
|
|
|
|
|
|
|
return md_render
|
2023-12-22 10:03:43 -07:00
|
|
|
|
|
2023-12-22 10:13:18 -07:00
|
|
|
|
|
2021-01-06 12:12:01 +00:00
|
|
|
|
def convert_to_boolean(value):
|
2021-06-02 15:17:29 +01:00
|
|
|
|
if isinstance(value, str):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if value.lower() in ["t", "true", "on", "yes", "1"]:
|
2021-01-06 12:12:01 +00:00
|
|
|
|
return True
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif value.lower() in ["f", "false", "off", "no", "0"]:
|
2021-01-06 12:12:01 +00:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_datetime(date):
|
2023-11-16 12:24:27 -08:00
|
|
|
|
return "{} at {} {}".format(
|
|
|
|
|
|
format_date(date), format_time_24h(date), get_user_preferred_timezone()
|
|
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_datetime_24h(date):
|
2023-11-16 12:24:27 -08:00
|
|
|
|
return "{} at {} {}".format(
|
|
|
|
|
|
format_date(date), format_time_24h(date), get_user_preferred_timezone()
|
2021-01-06 12:12:01 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-06-26 14:07:28 -07:00
|
|
|
|
def format_time(date):
|
|
|
|
|
|
return format_datetime_24h(date)
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-01-06 12:12:01 +00:00
|
|
|
|
def format_datetime_normal(date):
|
2024-03-18 15:10:26 -07:00
|
|
|
|
# example: February 20, 2024 at 07:00 PM US/Eastern, used for datetimes that's not within tables
|
2023-11-16 12:24:27 -08:00
|
|
|
|
return "{} at {} {}".format(
|
2024-03-18 15:10:26 -07:00
|
|
|
|
format_date_normal(date), format_time_12h(date), get_user_preferred_timezone()
|
2023-11-16 12:24:27 -08:00
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
2024-04-09 12:52:45 -07:00
|
|
|
|
def format_datetime_scheduled_notification(date):
|
|
|
|
|
|
# e.g. April 09, 2024 at 04:00 PM US/Eastern.
|
|
|
|
|
|
# Everything except scheduled notifications, the time is always "now".
|
|
|
|
|
|
# Scheduled notifications are the exception to the rule.
|
|
|
|
|
|
# Here we are formating and displaying the datetime without converting datetime to a different timezone.
|
|
|
|
|
|
|
|
|
|
|
|
datetime_obj = parse_naive_dt(date)
|
|
|
|
|
|
|
|
|
|
|
|
format_time_without_tz = datetime_obj.replace(tzinfo=timezone.utc).strftime(
|
|
|
|
|
|
"%I:%M %p"
|
|
|
|
|
|
)
|
|
|
|
|
|
return "{} at {} {}".format(
|
|
|
|
|
|
format_date_normal(date), format_time_without_tz, get_user_preferred_timezone()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-03-18 15:10:26 -07:00
|
|
|
|
def format_datetime_table(date):
|
|
|
|
|
|
# example: 03-18-2024 at 04:53 PM, intended for datetimes in tables
|
|
|
|
|
|
return "{} at {}".format(format_date_numeric(date), format_time_12h(date))
|
2024-01-25 15:32:44 -08:00
|
|
|
|
|
2024-01-23 16:18:43 -08:00
|
|
|
|
|
|
|
|
|
|
def format_time_12h(date):
|
2024-04-09 12:52:45 -07:00
|
|
|
|
date = parse_naive_dt(date)
|
2024-01-23 16:18:43 -08:00
|
|
|
|
|
|
|
|
|
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
2024-04-09 12:52:45 -07:00
|
|
|
|
return (
|
|
|
|
|
|
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
|
|
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
2024-01-25 15:32:44 -08:00
|
|
|
|
|
2021-01-06 12:12:01 +00:00
|
|
|
|
def format_datetime_relative(date):
|
2023-11-16 12:24:27 -08:00
|
|
|
|
return "{} at {} {}".format(
|
|
|
|
|
|
get_human_day(date), format_time_24h(date), get_user_preferred_timezone()
|
|
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_datetime_numeric(date):
|
2023-11-16 12:24:27 -08:00
|
|
|
|
return "{} {} {}".format(
|
|
|
|
|
|
format_date_numeric(date), format_time_24h(date), get_user_preferred_timezone()
|
2021-01-06 12:12:01 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_date_numeric(date):
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2023-11-16 12:24:27 -08:00
|
|
|
|
|
2023-11-21 13:40:00 -08:00
|
|
|
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
|
|
|
|
|
return (
|
2024-03-18 14:32:32 -07:00
|
|
|
|
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
|
2023-11-21 13:40:00 -08:00
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_time_24h(date):
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2023-11-16 12:24:27 -08:00
|
|
|
|
|
2023-11-21 13:40:00 -08:00
|
|
|
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
|
|
|
|
|
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%H:%M")
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def get_human_day(time, date_prefix=""):
|
2021-01-06 12:12:01 +00:00
|
|
|
|
# Add 1 minute to transform 00:00 into ‘midnight today’ instead of ‘midnight tomorrow’
|
2022-11-28 20:39:24 -05:00
|
|
|
|
time = parse_naive_dt(time)
|
2023-11-21 13:40:00 -08:00
|
|
|
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
|
|
|
|
|
time = time.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
|
2023-06-12 15:49:48 -04:00
|
|
|
|
date = (time - timedelta(minutes=1)).date()
|
2023-11-21 13:40:00 -08:00
|
|
|
|
now = datetime.now(preferred_tz)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
if date == (now + timedelta(days=1)).date():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "tomorrow"
|
2021-01-06 12:12:01 +00:00
|
|
|
|
if date == now.date():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "today"
|
2021-01-06 12:12:01 +00:00
|
|
|
|
if date == (now - timedelta(days=1)).date():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "yesterday"
|
|
|
|
|
|
if date.strftime("%Y") != now.strftime("%Y"):
|
|
|
|
|
|
return "{} {} {}".format(
|
2021-01-06 12:12:01 +00:00
|
|
|
|
date_prefix,
|
|
|
|
|
|
_format_datetime_short(date),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
date.strftime("%Y"),
|
2021-01-06 12:12:01 +00:00
|
|
|
|
).strip()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "{} {}".format(
|
2021-01-06 12:12:01 +00:00
|
|
|
|
date_prefix,
|
|
|
|
|
|
_format_datetime_short(date),
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_date(date):
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2023-11-21 13:40:00 -08:00
|
|
|
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
|
|
|
|
|
return (
|
|
|
|
|
|
date.replace(tzinfo=timezone.utc)
|
|
|
|
|
|
.astimezone(preferred_tz)
|
|
|
|
|
|
.strftime("%A %d %B %Y")
|
|
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_date_normal(date):
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2024-03-18 15:10:26 -07:00
|
|
|
|
return date.strftime("%B %d, %Y").lstrip("0")
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_date_short(date):
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2023-11-21 13:40:00 -08:00
|
|
|
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
|
|
|
|
|
return _format_datetime_short(
|
|
|
|
|
|
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
|
|
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_date_human(date):
|
|
|
|
|
|
return get_human_day(date)
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def format_datetime_human(date, date_prefix=""):
|
2023-11-16 12:24:27 -08:00
|
|
|
|
return "{} at {} {}".format(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
get_human_day(date, date_prefix="on"),
|
2023-06-26 08:42:04 -07:00
|
|
|
|
format_time_24h(date),
|
2023-11-16 12:24:27 -08:00
|
|
|
|
get_user_preferred_timezone(),
|
2021-01-06 12:12:01 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_day_of_week(date):
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2023-11-21 13:40:00 -08:00
|
|
|
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
|
|
|
|
|
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%A")
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_datetime_short(datetime):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return datetime.strftime("%d %B").lstrip("0")
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def naturaltime_without_indefinite_article(date):
|
|
|
|
|
|
return re.sub(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"an? (.*) ago",
|
|
|
|
|
|
lambda match: "1 {} ago".format(match.group(1)),
|
2021-01-06 12:12:01 +00:00
|
|
|
|
humanize.naturaltime(date),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_delta(date):
|
2022-11-30 16:42:42 -05:00
|
|
|
|
# This method assumes that date is in UTC
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
delta = (datetime.utcnow()) - (date)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
if delta < timedelta(seconds=30):
|
|
|
|
|
|
return "just now"
|
|
|
|
|
|
if delta < timedelta(seconds=60):
|
|
|
|
|
|
return "in the last minute"
|
|
|
|
|
|
return naturaltime_without_indefinite_article(delta)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_delta_days(date):
|
2022-11-30 16:42:42 -05:00
|
|
|
|
# This method assumes that date is in UTC
|
2022-11-28 20:39:24 -05:00
|
|
|
|
date = parse_naive_dt(date)
|
2022-11-30 16:42:42 -05:00
|
|
|
|
now = datetime.utcnow()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if date.strftime("%Y-%m-%d") == now.strftime("%Y-%m-%d"):
|
2021-01-06 12:12:01 +00:00
|
|
|
|
return "today"
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if date.strftime("%Y-%m-%d") == (now - timedelta(days=1)).strftime("%Y-%m-%d"):
|
2021-01-06 12:12:01 +00:00
|
|
|
|
return "yesterday"
|
|
|
|
|
|
return naturaltime_without_indefinite_article(now - date)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def valid_phone_number(phone_number):
|
|
|
|
|
|
try:
|
|
|
|
|
|
validate_phone_number(phone_number)
|
|
|
|
|
|
return True
|
|
|
|
|
|
except InvalidPhoneError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_notification_type(notification_type):
|
|
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"email": "Email",
|
|
|
|
|
|
"sms": "Text message",
|
2021-01-06 12:12:01 +00:00
|
|
|
|
}[notification_type]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_notification_status(status, template_type):
|
|
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"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",
|
2021-01-06 12:12:01 +00:00
|
|
|
|
},
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"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",
|
2021-01-06 12:12:01 +00:00
|
|
|
|
},
|
|
|
|
|
|
}[template_type].get(status, status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_notification_status_as_time(status, created, updated):
|
|
|
|
|
|
return dict.fromkeys(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
{"created", "pending", "sending"}, " since {}".format(created)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
).get(status, updated)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_notification_status_as_field_status(status, notification_type):
|
2022-12-12 15:29:04 -05:00
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"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",
|
|
|
|
|
|
}.get(status, "error")
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_notification_status_as_url(status, notification_type):
|
|
|
|
|
|
url = partial(url_for, "main.message_status")
|
|
|
|
|
|
|
|
|
|
|
|
if status not in {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"technical-failure",
|
|
|
|
|
|
"temporary-failure",
|
|
|
|
|
|
"permanent-failure",
|
2021-01-06 12:12:01 +00:00
|
|
|
|
}:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"email": url(_anchor="email-statuses"),
|
|
|
|
|
|
"sms": url(_anchor="text-message-statuses"),
|
2021-01-06 12:12:01 +00:00
|
|
|
|
}.get(notification_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def nl2br(value):
|
|
|
|
|
|
if value:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return Markup(
|
|
|
|
|
|
Take(
|
|
|
|
|
|
Field(
|
|
|
|
|
|
value,
|
|
|
|
|
|
html="escape",
|
|
|
|
|
|
)
|
|
|
|
|
|
).then(utils_nl2br)
|
|
|
|
|
|
)
|
|
|
|
|
|
return ""
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
2022-10-12 20:16:22 +00:00
|
|
|
|
# this formatter appears to only be used in the letter module
|
2022-12-05 15:33:44 -05:00
|
|
|
|
# TODO: use more widely, or delete? currency symbol could be set in config
|
2021-01-06 12:12:01 +00:00
|
|
|
|
def format_number_in_pounds_as_currency(number):
|
|
|
|
|
|
if number >= 1:
|
|
|
|
|
|
return f"£{number:,.2f}"
|
|
|
|
|
|
|
|
|
|
|
|
return f"{number * 100:.0f}p"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_list_items(items, format_string, *args, **kwargs):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Apply formatting to each item in an iterable. Returns a list.
|
|
|
|
|
|
Each item is made available in the format_string as the 'item' keyword argument.
|
|
|
|
|
|
example usage: ['png','svg','pdf']|format_list_items('{0}. {item}', [1,2,3]) -> ['1. png', '2. svg', '3. pdf']
|
|
|
|
|
|
"""
|
|
|
|
|
|
return [format_string.format(*args, item=item, **kwargs) for item in items]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def linkable_name(value):
|
|
|
|
|
|
return urllib.parse.quote_plus(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_thousands(value):
|
|
|
|
|
|
if isinstance(value, Number):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "{:,.0f}".format(value)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
if value is None:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return ""
|
2021-01-06 12:12:01 +00:00
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def email_safe(string, whitespace="."):
|
2021-01-06 12:12:01 +00:00
|
|
|
|
# strips accents, diacritics etc
|
2023-08-25 09:12:23 -07:00
|
|
|
|
string = "".join(
|
|
|
|
|
|
c
|
|
|
|
|
|
for c in unicodedata.normalize("NFD", string)
|
|
|
|
|
|
if unicodedata.category(c) != "Mn"
|
|
|
|
|
|
)
|
|
|
|
|
|
string = "".join(
|
|
|
|
|
|
word.lower() if word.isalnum() or word == whitespace else ""
|
|
|
|
|
|
for word in re.sub(r"\s+", whitespace, string.strip())
|
2021-01-06 12:12:01 +00:00
|
|
|
|
)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
string = re.sub(r"\.{2,}", ".", string)
|
|
|
|
|
|
return string.strip(".")
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def id_safe(string):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return email_safe(string, whitespace="-")
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def round_to_significant_figures(value, number_of_significant_figures):
|
|
|
|
|
|
if value == 0:
|
|
|
|
|
|
return value
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return int(
|
|
|
|
|
|
round(value, number_of_significant_figures - int(floor(log10(abs(value)))) - 1)
|
|
|
|
|
|
)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def redact_mobile_number(mobile_number, spacing=""):
|
|
|
|
|
|
indices = [-4, -5, -6, -7]
|
|
|
|
|
|
redact_character = spacing + "•" + spacing
|
|
|
|
|
|
mobile_number_list = list(mobile_number.replace(" ", ""))
|
|
|
|
|
|
for i in indices:
|
|
|
|
|
|
mobile_number_list[i] = redact_character
|
|
|
|
|
|
return "".join(mobile_number_list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_time_left(created_at, service_data_retention_days=7):
|
|
|
|
|
|
return ago.human(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
(datetime.now(timezone.utc))
|
|
|
|
|
|
- (
|
|
|
|
|
|
dateutil.parser.parse(created_at).replace(hour=0, minute=0, second=0)
|
|
|
|
|
|
+ timedelta(days=service_data_retention_days + 1)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
future_tense="Data available for {}",
|
|
|
|
|
|
past_tense="Data no longer available", # No-one should ever see this
|
|
|
|
|
|
precision=1,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def starts_with_initial(name):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return bool(re.match(r"^.\.", name))
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_middle_initial(name):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return re.sub(r"\s+.\s+", " ", name)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_digits(name):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "".join(c for c in name if not c.isdigit())
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_spaces(name):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return " ".join(name.split())
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def guess_name_from_email_address(email_address):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
possible_name = re.split(r"[\@\+]", email_address)[0]
|
|
|
|
|
|
|
|
|
|
|
|
if "." not in possible_name or starts_with_initial(possible_name):
|
|
|
|
|
|
return ""
|
2021-01-06 12:12:01 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return (
|
|
|
|
|
|
Take(possible_name)
|
|
|
|
|
|
.then(str.replace, ".", " ")
|
|
|
|
|
|
.then(remove_digits)
|
|
|
|
|
|
.then(remove_middle_initial)
|
|
|
|
|
|
.then(str.title)
|
|
|
|
|
|
.then(make_quotes_smart)
|
|
|
|
|
|
.then(normalize_spaces)
|
2021-01-06 12:12:01 +00:00
|
|
|
|
)
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def message_count_label(count, template_type, suffix="sent"):
|
2021-01-06 12:59:48 +00:00
|
|
|
|
if suffix:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return f"{message_count_noun(count, template_type)} {suffix}"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
return message_count_noun(count, template_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def message_count_noun(count, template_type):
|
|
|
|
|
|
if template_type is None:
|
|
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "message"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "messages"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if template_type == "sms":
|
2021-01-06 12:59:48 +00:00
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "text message"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "text messages"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
2024-01-25 14:55:17 -05:00
|
|
|
|
if template_type == "parts":
|
|
|
|
|
|
if count == 1:
|
|
|
|
|
|
return "text message part"
|
|
|
|
|
|
else:
|
|
|
|
|
|
return "text message parts"
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif template_type == "email":
|
2021-01-06 12:59:48 +00:00
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "email"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "emails"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def message_count(count, template_type):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return f"{format_thousands(count)} " f"{message_count_noun(count, template_type)}"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def recipient_count_label(count, template_type):
|
|
|
|
|
|
if template_type is None:
|
|
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "recipient"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "recipients"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if template_type == "sms":
|
2021-01-06 12:59:48 +00:00
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "phone number"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "phone numbers"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif template_type == "email":
|
2021-01-06 12:59:48 +00:00
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "email address"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "email addresses"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def recipient_count(count, template_type):
|
|
|
|
|
|
return (
|
2023-08-25 09:12:23 -07:00
|
|
|
|
f"{format_thousands(count)} " f"{recipient_count_label(count, template_type)}"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def iteration_count(count):
|
|
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "once"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
elif count == 2:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "twice"
|
2021-01-06 12:59:48 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return f"{count} times"
|
2021-01-06 11:54:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def character_count(count):
|
|
|
|
|
|
if count == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "1 character"
|
|
|
|
|
|
return f"{format_thousands(count)} characters"
|
2021-02-15 21:02:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_mobile_network(network):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if network in ("three", "vodafone", "o2"):
|
2021-02-15 21:02:37 +00:00
|
|
|
|
return network.capitalize()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "EE"
|
2021-03-11 13:23:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_billions(count):
|
|
|
|
|
|
return humanize.intword(count)
|
2021-10-11 14:26:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def format_yes_no(value, yes="Yes", no="No", none="No"):
|
2021-10-11 15:24:55 +01:00
|
|
|
|
if value is None:
|
|
|
|
|
|
return none
|
|
|
|
|
|
return yes if value else no
|
2021-08-03 18:46:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def square_metres_to_square_miles(area):
|
|
|
|
|
|
return area * 3.86e-7
|
2022-03-14 14:39:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_auth_type(auth_type, with_indefinite_article=False):
|
|
|
|
|
|
indefinite_article, auth_type = {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"email_auth": ("an", "Email link"),
|
|
|
|
|
|
"sms_auth": ("a", "Text message code"),
|
2022-03-15 10:40:10 +00:00
|
|
|
|
}[auth_type]
|
2022-03-14 14:39:54 +00:00
|
|
|
|
|
|
|
|
|
|
if with_indefinite_article:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return f"{indefinite_article} {auth_type.lower()}"
|
2022-03-14 14:39:54 +00:00
|
|
|
|
|
|
|
|
|
|
return auth_type
|