2025-07-24 10:49:30 -07:00
|
|
|
|
import os
|
|
|
|
|
|
import pathlib
|
|
|
|
|
|
import re
|
|
|
|
|
|
import secrets
|
2025-07-29 17:33:13 -04:00
|
|
|
|
import sys
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from functools import partial
|
|
|
|
|
|
from time import monotonic
|
|
|
|
|
|
from urllib.parse import unquote, urlparse, urlunparse
|
2016-10-19 11:54:51 +01:00
|
|
|
|
|
2025-07-24 10:49:30 -07:00
|
|
|
|
import jinja2
|
|
|
|
|
|
from flask import (
|
2016-04-04 16:53:52 +01:00
|
|
|
|
current_app,
|
2019-03-25 10:25:05 +00:00
|
|
|
|
flash,
|
2016-07-11 13:53:55 +01:00
|
|
|
|
g,
|
2019-03-25 10:25:05 +00:00
|
|
|
|
make_response,
|
2020-03-06 11:53:55 +00:00
|
|
|
|
redirect,
|
2019-03-25 10:25:05 +00:00
|
|
|
|
render_template,
|
|
|
|
|
|
request,
|
|
|
|
|
|
session,
|
2022-07-01 11:49:31 -07:00
|
|
|
|
url_for,
|
2017-10-18 14:51:26 +01:00
|
|
|
|
)
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from flask.globals import request_ctx
|
|
|
|
|
|
from flask_login import LoginManager, current_user
|
|
|
|
|
|
from flask_talisman import Talisman
|
|
|
|
|
|
from flask_wtf import CSRFProtect
|
|
|
|
|
|
from flask_wtf.csrf import CSRFError
|
|
|
|
|
|
from itsdangerous import BadSignature
|
|
|
|
|
|
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
|
|
|
|
|
|
from werkzeug.exceptions import abort
|
|
|
|
|
|
from werkzeug.local import LocalProxy
|
2016-03-10 14:56:47 +00:00
|
|
|
|
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from app import proxy_fix
|
|
|
|
|
|
from app.asset_fingerprinter import asset_fingerprinter
|
|
|
|
|
|
from app.config import configs
|
|
|
|
|
|
from app.extensions import redis_client
|
|
|
|
|
|
from app.formatters import (
|
2023-12-27 12:59:45 -07:00
|
|
|
|
convert_markdown_template,
|
2025-06-25 13:25:01 -07:00
|
|
|
|
convert_time_unixtimestamp,
|
2025-06-25 14:32:54 -07:00
|
|
|
|
convert_to_boolean,
|
2022-03-14 14:39:54 +00:00
|
|
|
|
format_auth_type,
|
2021-03-11 13:23:02 +00:00
|
|
|
|
format_billions,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
format_date,
|
|
|
|
|
|
format_date_human,
|
|
|
|
|
|
format_date_normal,
|
|
|
|
|
|
format_date_numeric,
|
|
|
|
|
|
format_date_short,
|
|
|
|
|
|
format_datetime,
|
|
|
|
|
|
format_datetime_24h,
|
|
|
|
|
|
format_datetime_human,
|
|
|
|
|
|
format_datetime_normal,
|
|
|
|
|
|
format_datetime_relative,
|
2024-04-09 12:52:45 -07:00
|
|
|
|
format_datetime_scheduled_notification,
|
2024-03-18 15:10:26 -07:00
|
|
|
|
format_datetime_table,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
format_day_of_week,
|
|
|
|
|
|
format_delta,
|
|
|
|
|
|
format_delta_days,
|
|
|
|
|
|
format_list_items,
|
2021-02-15 21:02:37 +00:00
|
|
|
|
format_mobile_network,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
format_notification_status,
|
|
|
|
|
|
format_notification_status_as_field_status,
|
|
|
|
|
|
format_notification_status_as_time,
|
|
|
|
|
|
format_notification_status_as_url,
|
|
|
|
|
|
format_notification_type,
|
|
|
|
|
|
format_number_in_pounds_as_currency,
|
|
|
|
|
|
format_thousands,
|
2023-06-26 14:07:28 -07:00
|
|
|
|
format_time_24h,
|
2021-10-11 14:26:46 +01:00
|
|
|
|
format_yes_no,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
id_safe,
|
2021-01-06 12:59:48 +00:00
|
|
|
|
iteration_count,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
linkable_name,
|
2021-01-06 12:59:48 +00:00
|
|
|
|
message_count,
|
|
|
|
|
|
message_count_label,
|
|
|
|
|
|
message_count_noun,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
nl2br,
|
2021-01-06 12:59:48 +00:00
|
|
|
|
recipient_count,
|
|
|
|
|
|
recipient_count_label,
|
2021-06-24 16:54:08 +01:00
|
|
|
|
round_to_significant_figures,
|
2021-08-03 18:46:30 +01:00
|
|
|
|
square_metres_to_square_miles,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
valid_phone_number,
|
|
|
|
|
|
)
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from app.models.organization import Organization
|
|
|
|
|
|
from app.models.service import Service
|
|
|
|
|
|
from app.models.user import AnonymousUser, User
|
|
|
|
|
|
from app.navigation import (
|
2018-06-12 16:17:20 +01:00
|
|
|
|
CaseworkNavigation,
|
|
|
|
|
|
HeaderNavigation,
|
|
|
|
|
|
MainNavigation,
|
2019-03-25 10:25:05 +00:00
|
|
|
|
OrgNavigation,
|
2024-01-03 13:51:11 -05:00
|
|
|
|
SecondaryNavigation,
|
2018-06-12 16:17:20 +01:00
|
|
|
|
)
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from app.notify_client import InviteTokenError
|
|
|
|
|
|
from app.notify_client.api_key_api_client import api_key_api_client
|
|
|
|
|
|
from app.notify_client.billing_api_client import billing_api_client
|
|
|
|
|
|
from app.notify_client.complaint_api_client import complaint_api_client
|
|
|
|
|
|
from app.notify_client.events_api_client import events_api_client
|
|
|
|
|
|
from app.notify_client.inbound_number_client import inbound_number_client
|
|
|
|
|
|
from app.notify_client.invite_api_client import invite_api_client
|
|
|
|
|
|
from app.notify_client.job_api_client import job_api_client
|
|
|
|
|
|
from app.notify_client.notification_api_client import notification_api_client
|
|
|
|
|
|
from app.notify_client.org_invite_api_client import org_invite_api_client
|
|
|
|
|
|
from app.notify_client.organizations_api_client import organizations_client
|
|
|
|
|
|
from app.notify_client.performance_dashboard_api_client import (
|
2021-03-10 11:01:15 +00:00
|
|
|
|
performance_dashboard_api_client,
|
2021-03-10 10:35:58 +00:00
|
|
|
|
)
|
2025-10-06 09:38:54 -04:00
|
|
|
|
from app.notify_client.platform_stats_api_client import platform_stats_api_client
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from app.notify_client.service_api_client import service_api_client
|
|
|
|
|
|
from app.notify_client.status_api_client import status_api_client
|
2025-10-06 09:38:54 -04:00
|
|
|
|
from app.notify_client.template_folder_api_client import template_folder_api_client
|
|
|
|
|
|
from app.notify_client.template_statistics_api_client import template_statistics_client
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from app.notify_client.upload_api_client import upload_api_client
|
|
|
|
|
|
from app.notify_client.user_api_client import user_api_client
|
|
|
|
|
|
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
|
|
|
|
|
|
from app.utils.api_health import is_api_down
|
2025-10-06 09:38:54 -04:00
|
|
|
|
from app.utils.nunjucks_jinja.flask_ext import init_nunjucks_environment
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from notifications_python_client.errors import HTTPError
|
|
|
|
|
|
from notifications_utils import logging, request_helper
|
|
|
|
|
|
from notifications_utils.formatters import (
|
2024-05-16 10:37:37 -04:00
|
|
|
|
formatted_list,
|
|
|
|
|
|
get_lines_with_normalised_whitespace,
|
|
|
|
|
|
)
|
2025-07-24 10:49:30 -07:00
|
|
|
|
from notifications_utils.recipients import format_phone_number_human_readable
|
|
|
|
|
|
from notifications_utils.url_safe_token import generate_token
|
2017-07-11 17:06:15 +01:00
|
|
|
|
|
2015-11-27 16:25:56 +00:00
|
|
|
|
login_manager = LoginManager()
|
2017-07-27 15:17:17 +01:00
|
|
|
|
csrf = CSRFProtect()
|
2023-03-07 14:56:31 -05:00
|
|
|
|
talisman = Talisman()
|
2022-08-05 00:25:03 -07:00
|
|
|
|
|
2016-04-04 16:53:52 +01:00
|
|
|
|
# The current service attached to the request stack.
|
2023-08-25 09:12:23 -07:00
|
|
|
|
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
2016-04-04 16:53:52 +01:00
|
|
|
|
|
2023-07-12 12:09:44 -04:00
|
|
|
|
# The current organization attached to the request stack.
|
2023-08-25 09:12:23 -07:00
|
|
|
|
current_organization = LocalProxy(partial(getattr, request_ctx, "organization"))
|
2015-11-20 16:22:44 +00:00
|
|
|
|
|
2018-04-25 11:11:37 +01:00
|
|
|
|
navigation = {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"casework_navigation": CaseworkNavigation(),
|
|
|
|
|
|
"main_navigation": MainNavigation(),
|
|
|
|
|
|
"header_navigation": HeaderNavigation(),
|
|
|
|
|
|
"org_navigation": OrgNavigation(),
|
2024-01-03 13:51:11 -05:00
|
|
|
|
"secondary_navigation": SecondaryNavigation(),
|
2018-04-25 11:11:37 +01:00
|
|
|
|
}
|
2018-04-25 10:24:32 +01:00
|
|
|
|
|
2023-03-07 16:08:51 -05:00
|
|
|
|
|
2023-03-07 14:56:31 -05:00
|
|
|
|
def _csp(config):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
asset_domain = config["ASSET_DOMAIN"]
|
2025-05-22 12:03:18 -07:00
|
|
|
|
api_public_url = config["API_PUBLIC_URL"]
|
2025-04-30 10:04:30 -07:00
|
|
|
|
|
2025-05-21 17:03:35 -07:00
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"default-src": ["'self'", asset_domain],
|
2024-12-04 14:10:59 -08:00
|
|
|
|
"frame-src": [
|
|
|
|
|
|
"https://www.youtube.com",
|
|
|
|
|
|
"https://www.youtube-nocookie.com",
|
2025-04-10 15:10:07 -04:00
|
|
|
|
"https://www.googletagmanager.com",
|
2024-12-04 14:10:59 -08:00
|
|
|
|
],
|
2023-04-19 15:34:38 -04:00
|
|
|
|
"frame-ancestors": "'none'",
|
|
|
|
|
|
"form-action": "'self'",
|
2023-03-07 14:56:31 -05:00
|
|
|
|
"script-src": [
|
|
|
|
|
|
"'self'",
|
|
|
|
|
|
asset_domain,
|
|
|
|
|
|
"https://js-agent.newrelic.com",
|
2023-04-19 15:34:38 -04:00
|
|
|
|
"https://gov-bam.nr-data.net",
|
2023-11-27 16:16:27 -05:00
|
|
|
|
"https://www.googletagmanager.com",
|
2023-12-12 16:45:21 -05:00
|
|
|
|
"https://www.google-analytics.com",
|
|
|
|
|
|
"https://dap.digitalgov.gov",
|
2023-03-07 14:56:31 -05:00
|
|
|
|
],
|
2025-09-24 13:56:11 -07:00
|
|
|
|
"connect-src": list(
|
|
|
|
|
|
dict.fromkeys(
|
|
|
|
|
|
[
|
|
|
|
|
|
"'self'",
|
|
|
|
|
|
"https://gov-bam.nr-data.net",
|
|
|
|
|
|
"https://www.google-analytics.com",
|
|
|
|
|
|
f"{api_public_url}",
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"style-src": ["'self'", asset_domain],
|
2025-07-29 17:33:13 -04:00
|
|
|
|
"img-src": ["'self'", asset_domain],
|
2023-03-07 14:56:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-29 11:59:52 -07:00
|
|
|
|
|
2017-11-06 13:07:21 +00:00
|
|
|
|
def create_app(application):
|
2025-04-07 14:45:28 -04:00
|
|
|
|
@application.after_request
|
2025-07-31 11:39:58 -04:00
|
|
|
|
def add_security_headers(response):
|
2025-07-31 13:38:50 -04:00
|
|
|
|
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
|
2025-04-07 14:45:28 -04:00
|
|
|
|
return response
|
2025-04-11 13:34:32 -07:00
|
|
|
|
|
2025-04-21 16:26:07 -07:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def inject_is_api_down():
|
|
|
|
|
|
return {"is_api_down": is_api_down()}
|
|
|
|
|
|
|
2025-10-01 11:54:36 -04:00
|
|
|
|
# @application.context_processor
|
|
|
|
|
|
# def inject_feature_flags():
|
2025-10-01 19:50:35 -04:00
|
|
|
|
# this is where feature flags can be easily added as a dictionary within context
|
2025-10-07 10:44:45 -07:00
|
|
|
|
# feature_enabled = application.config.get("FEATURE_ENABLED", True)
|
2025-10-01 11:54:36 -04:00
|
|
|
|
|
2024-11-19 09:41:08 -05:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def inject_initial_signin_url():
|
2024-11-21 17:52:44 -05:00
|
|
|
|
ttl = 24 * 60 * 60
|
|
|
|
|
|
|
|
|
|
|
|
# make and store the state
|
|
|
|
|
|
state = generate_token(
|
2024-11-25 11:51:01 -05:00
|
|
|
|
str(request.remote_addr),
|
|
|
|
|
|
current_app.config["SECRET_KEY"],
|
|
|
|
|
|
current_app.config["DANGEROUS_SALT"],
|
2024-11-21 17:52:44 -05:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
state_key = f"login-state-{unquote(state)}"
|
|
|
|
|
|
redis_client.set(state_key, state, ex=ttl)
|
|
|
|
|
|
|
2024-11-25 11:51:01 -05:00
|
|
|
|
# make and store the nonce
|
2024-11-21 17:52:44 -05:00
|
|
|
|
nonce = secrets.token_urlsafe()
|
|
|
|
|
|
nonce_key = f"login-nonce-{unquote(nonce)}"
|
|
|
|
|
|
redis_client.set(nonce_key, nonce, ex=ttl)
|
|
|
|
|
|
|
2024-11-19 09:41:08 -05:00
|
|
|
|
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
2024-11-21 17:52:44 -05:00
|
|
|
|
if url is not None:
|
|
|
|
|
|
url = url.replace("NONCE", nonce)
|
|
|
|
|
|
url = url.replace("STATE", state)
|
|
|
|
|
|
|
2024-12-13 12:37:11 -05:00
|
|
|
|
return {"initial_signin_url": url}
|
2024-11-19 09:41:08 -05:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
notify_environment = os.environ["NOTIFY_ENVIRONMENT"]
|
2016-12-08 16:50:37 +00:00
|
|
|
|
|
|
|
|
|
|
application.config.from_object(configs[notify_environment])
|
2023-08-25 09:12:23 -07:00
|
|
|
|
asset_fingerprinter._asset_root = application.config["ASSET_PATH"]
|
2016-03-17 13:45:59 +00:00
|
|
|
|
|
|
|
|
|
|
init_app(application)
|
2019-01-29 11:18:08 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if "extensions" not in application.jinja_options:
|
|
|
|
|
|
application.jinja_options["extensions"] = []
|
2022-05-27 14:27:43 +01:00
|
|
|
|
|
2025-10-06 09:38:54 -04:00
|
|
|
|
init_nunjucks_environment(application)
|
2019-10-03 13:00:32 +01:00
|
|
|
|
init_jinja(application)
|
|
|
|
|
|
|
2019-01-29 11:18:08 +00:00
|
|
|
|
for client in (
|
|
|
|
|
|
csrf,
|
|
|
|
|
|
login_manager,
|
|
|
|
|
|
proxy_fix,
|
2019-01-30 13:45:05 +00:00
|
|
|
|
request_helper,
|
2019-02-14 14:25:31 +00:00
|
|
|
|
# API clients
|
2019-01-30 13:45:05 +00:00
|
|
|
|
api_key_api_client,
|
|
|
|
|
|
billing_api_client,
|
|
|
|
|
|
complaint_api_client,
|
|
|
|
|
|
events_api_client,
|
|
|
|
|
|
inbound_number_client,
|
|
|
|
|
|
invite_api_client,
|
|
|
|
|
|
job_api_client,
|
|
|
|
|
|
notification_api_client,
|
|
|
|
|
|
org_invite_api_client,
|
2023-07-12 12:09:44 -04:00
|
|
|
|
organizations_client,
|
2021-03-10 11:01:15 +00:00
|
|
|
|
performance_dashboard_api_client,
|
2019-01-30 13:45:05 +00:00
|
|
|
|
platform_stats_api_client,
|
|
|
|
|
|
service_api_client,
|
|
|
|
|
|
status_api_client,
|
|
|
|
|
|
template_folder_api_client,
|
|
|
|
|
|
template_statistics_client,
|
2020-05-11 10:52:43 +01:00
|
|
|
|
upload_api_client,
|
2019-01-30 13:45:05 +00:00
|
|
|
|
user_api_client,
|
|
|
|
|
|
# External API clients
|
2020-07-01 13:27:12 +01:00
|
|
|
|
redis_client,
|
2019-01-29 11:18:08 +00:00
|
|
|
|
):
|
|
|
|
|
|
client.init_app(application)
|
|
|
|
|
|
|
2023-03-07 14:56:31 -05:00
|
|
|
|
talisman.init_app(
|
|
|
|
|
|
application,
|
|
|
|
|
|
content_security_policy=_csp(application.config),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
content_security_policy_nonce_in=["style-src", "script-src"],
|
2023-04-19 15:34:38 -04:00
|
|
|
|
permissions_policy={
|
2025-07-31 13:38:50 -04:00
|
|
|
|
"accelerometer": '(self "https://www.youtube-nocookie.com")',
|
|
|
|
|
|
"autoplay": '(self "https://www.youtube-nocookie.com")',
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"camera": "()",
|
|
|
|
|
|
"geolocation": "()",
|
2025-07-31 13:38:50 -04:00
|
|
|
|
"gyroscope": '(self "https://www.youtube-nocookie.com")',
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"local-fonts": "()",
|
|
|
|
|
|
"magnetometer": "()",
|
|
|
|
|
|
"microphone": "()",
|
|
|
|
|
|
"midi": "()",
|
|
|
|
|
|
"payment": "()",
|
|
|
|
|
|
"screen-wake-lock": "()",
|
2023-04-19 15:34:38 -04:00
|
|
|
|
},
|
2023-08-25 09:12:23 -07:00
|
|
|
|
frame_options="deny",
|
|
|
|
|
|
force_https=(application.config["HTTP_PROTOCOL"] == "https"),
|
2023-03-07 14:56:31 -05:00
|
|
|
|
)
|
2020-07-01 13:27:12 +01:00
|
|
|
|
logging.init_app(application)
|
2015-11-30 11:21:51 +00:00
|
|
|
|
|
2024-06-17 10:13:15 -04:00
|
|
|
|
# Hopefully will help identify if there is a race condition causing the CSRF errors
|
|
|
|
|
|
# that we have occasionally seen in our environments.
|
2024-06-07 11:55:26 -04:00
|
|
|
|
for key in ("SECRET_KEY", "DANGEROUS_SALT"):
|
2024-06-17 10:13:15 -04:00
|
|
|
|
try:
|
|
|
|
|
|
value = application.config[key]
|
|
|
|
|
|
except KeyError:
|
|
|
|
|
|
application.logger.error(f"Env Var {key} doesn't exist.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
data_len = len(value.strip())
|
2024-06-17 12:15:00 -04:00
|
|
|
|
except (TypeError, AttributeError):
|
2024-06-17 10:13:15 -04:00
|
|
|
|
application.logger.error(f"Env Var {key} invalid type: {type(value)}")
|
|
|
|
|
|
else:
|
2024-06-18 10:07:00 -04:00
|
|
|
|
if data_len:
|
|
|
|
|
|
application.logger.info(f"Env Var {key} is a non-zero length.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
application.logger.error(f"Env Var {key} is empty.")
|
2024-06-07 11:55:26 -04:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
login_manager.login_view = "main.sign_in"
|
|
|
|
|
|
login_manager.login_message_category = "default"
|
2016-12-14 14:07:08 +00:00
|
|
|
|
login_manager.session_protection = None
|
2017-02-17 14:06:09 +00:00
|
|
|
|
login_manager.anonymous_user = AnonymousUser
|
2022-07-01 11:36:47 -07:00
|
|
|
|
|
2019-02-14 14:25:31 +00:00
|
|
|
|
# make sure we handle unicode correctly
|
|
|
|
|
|
redis_client.redis_store.decode_responses = True
|
|
|
|
|
|
|
2023-05-02 09:45:48 -04:00
|
|
|
|
from app.main import main as main_blueprint
|
|
|
|
|
|
from app.status import status as status_blueprint
|
|
|
|
|
|
|
|
|
|
|
|
application.register_blueprint(main_blueprint)
|
|
|
|
|
|
|
|
|
|
|
|
application.register_blueprint(status_blueprint)
|
2016-01-11 14:54:23 +00:00
|
|
|
|
|
2017-11-06 13:07:21 +00:00
|
|
|
|
add_template_filters(application)
|
2016-04-21 09:30:33 +01:00
|
|
|
|
|
2016-01-07 15:48:29 +00:00
|
|
|
|
register_errorhandlers(application)
|
2016-01-07 13:58:38 +00:00
|
|
|
|
|
2016-04-27 16:39:17 +01:00
|
|
|
|
setup_event_handlers()
|
|
|
|
|
|
|
2015-11-24 09:40:14 +00:00
|
|
|
|
|
2016-03-17 13:45:59 +00:00
|
|
|
|
def init_app(application):
|
2023-09-14 14:59:48 -06:00
|
|
|
|
application.before_request(redirect_notify_to_beta)
|
2017-11-06 13:07:21 +00:00
|
|
|
|
application.before_request(load_service_before_request)
|
2023-07-12 12:09:44 -04:00
|
|
|
|
application.before_request(load_organization_before_request)
|
2017-11-16 16:25:40 +00:00
|
|
|
|
application.before_request(request_helper.check_proxy_header_before_request)
|
2023-05-02 09:45:48 -04:00
|
|
|
|
application.before_request(make_session_permanent)
|
|
|
|
|
|
application.after_request(save_service_or_org_after_request)
|
2017-11-06 13:07:21 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
start = len(asset_fingerprinter._filesystem_path)
|
2020-12-29 13:38:27 +00:00
|
|
|
|
font_paths = [
|
2023-08-25 09:12:23 -07:00
|
|
|
|
str(item)[start:]
|
|
|
|
|
|
for item in pathlib.Path(asset_fingerprinter._filesystem_path).glob(
|
|
|
|
|
|
"fonts/*.woff2"
|
|
|
|
|
|
)
|
2020-12-29 13:38:27 +00:00
|
|
|
|
]
|
|
|
|
|
|
|
2017-11-06 13:07:21 +00:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _attach_current_service():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return {"current_service": current_service}
|
2016-05-12 13:56:14 +01:00
|
|
|
|
|
2018-02-13 14:49:03 +00:00
|
|
|
|
@application.context_processor
|
2023-07-12 12:09:44 -04:00
|
|
|
|
def _attach_current_organization():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return {"current_org": current_organization}
|
2018-02-13 14:49:03 +00:00
|
|
|
|
|
2018-01-25 15:29:05 +00:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _attach_current_user():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return {"current_user": current_user}
|
2018-01-25 15:29:05 +00:00
|
|
|
|
|
2025-07-15 23:56:58 -07:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _attach_enums():
|
|
|
|
|
|
from app.enums import ServicePermission
|
2025-07-22 11:01:20 -07:00
|
|
|
|
|
2025-07-15 23:56:58 -07:00
|
|
|
|
return {"ServicePermission": ServicePermission}
|
|
|
|
|
|
|
2018-04-24 12:48:05 +01:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _nav_selected():
|
2018-04-25 11:11:37 +01:00
|
|
|
|
return navigation
|
2018-04-24 12:48:05 +01:00
|
|
|
|
|
2023-06-01 10:44:13 -06:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _attach_current_global_daily_messages():
|
2023-11-20 13:45:35 -05:00
|
|
|
|
global_limit = 0
|
2023-06-01 10:44:13 -06:00
|
|
|
|
remaining_global_messages = 0
|
|
|
|
|
|
if current_app:
|
2023-10-27 10:23:59 -06:00
|
|
|
|
if request.view_args:
|
2023-10-27 10:42:09 -06:00
|
|
|
|
service_id = request.view_args.get(
|
|
|
|
|
|
"service_id", session.get("service_id")
|
|
|
|
|
|
)
|
2023-10-27 10:23:59 -06:00
|
|
|
|
else:
|
|
|
|
|
|
service_id = session.get("service_id")
|
2023-10-17 11:11:17 -06:00
|
|
|
|
|
|
|
|
|
|
if service_id:
|
|
|
|
|
|
global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"]
|
2023-10-27 10:00:45 -06:00
|
|
|
|
global_messages_count = (
|
|
|
|
|
|
service_api_client.get_global_notification_count(service_id)
|
|
|
|
|
|
)
|
|
|
|
|
|
remaining_global_messages = global_limit - global_messages_count.get(
|
|
|
|
|
|
"count"
|
|
|
|
|
|
)
|
2023-11-16 12:02:59 -05:00
|
|
|
|
return {
|
2023-11-16 12:26:35 -05:00
|
|
|
|
"global_message_limit": global_limit,
|
2023-11-16 12:02:59 -05:00
|
|
|
|
"daily_global_messages_remaining": remaining_global_messages,
|
|
|
|
|
|
}
|
2023-05-30 08:01:29 -06:00
|
|
|
|
|
2016-05-12 13:56:14 +01:00
|
|
|
|
@application.before_request
|
|
|
|
|
|
def record_start_time():
|
|
|
|
|
|
g.start = monotonic()
|
2016-12-01 17:21:06 +00:00
|
|
|
|
g.endpoint = request.endpoint
|
2016-05-12 13:56:14 +01:00
|
|
|
|
|
2016-03-17 13:45:59 +00:00
|
|
|
|
@application.context_processor
|
2015-12-15 08:20:25 +00:00
|
|
|
|
def inject_global_template_variables():
|
2016-02-01 14:46:12 +00:00
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"asset_path": application.config["ASSET_PATH"],
|
|
|
|
|
|
"header_colour": application.config["HEADER_COLOUR"],
|
|
|
|
|
|
"asset_url": asset_fingerprinter.get_url,
|
|
|
|
|
|
"font_paths": font_paths,
|
2016-02-01 14:46:12 +00:00
|
|
|
|
}
|
2015-12-15 08:20:25 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
application.url_map.converters["uuid"].to_python = lambda self, value: value
|
|
|
|
|
|
application.url_map.converters["template_type"] = TemplateTypeConverter
|
|
|
|
|
|
application.url_map.converters["simple_date"] = SimpleDateTypeConverter
|
2019-11-04 12:20:09 +00:00
|
|
|
|
|
2015-11-25 15:29:12 +00:00
|
|
|
|
|
2016-03-30 09:58:10 +01:00
|
|
|
|
@login_manager.user_loader
|
|
|
|
|
|
def load_user(user_id):
|
2019-05-23 15:27:35 +01:00
|
|
|
|
return User.from_id(user_id)
|
2016-03-30 09:58:10 +01:00
|
|
|
|
|
|
|
|
|
|
|
2019-11-28 14:39:30 +00:00
|
|
|
|
def make_session_permanent():
|
|
|
|
|
|
"""
|
|
|
|
|
|
Make sessions permanent. By permanent, we mean "admin app sets when it expires". Normally the cookie would expire
|
|
|
|
|
|
whenever you close the browser. With this, the session expiry is set in `config['PERMANENT_SESSION_LIFETIME']`
|
2023-08-16 10:55:24 -06:00
|
|
|
|
(30 min) and is refreshed after every request. IE: you will be logged out after thirty minutes of inactivity.
|
2019-11-28 14:39:30 +00:00
|
|
|
|
|
|
|
|
|
|
We don't _need_ to set this every request (it's saved within the cookie itself under the `_permanent` flag), only
|
|
|
|
|
|
when you first log in/sign up/get invited/etc, but we do it just to be safe. For more reading, check here:
|
|
|
|
|
|
https://stackoverflow.com/questions/34118093/flask-permanent-session-where-to-define-them
|
|
|
|
|
|
"""
|
|
|
|
|
|
session.permanent = True
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-09-18 06:24:17 -06:00
|
|
|
|
def create_beta_url(url):
|
2024-06-10 10:53:09 -07:00
|
|
|
|
url_created = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
url_created = urlparse(url)
|
|
|
|
|
|
url_list = list(url_created)
|
|
|
|
|
|
url_list[1] = "beta.notify.gov"
|
|
|
|
|
|
url_for_redirect = urlunparse(url_list)
|
|
|
|
|
|
return url_for_redirect
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
# This might be happening due to IPv6, see issue # 1395.
|
|
|
|
|
|
# If we see "'RequestContext' object has no attribute 'service'" in the logs
|
|
|
|
|
|
# we can search around that timestamp and find this output, hopefully.
|
|
|
|
|
|
# It may be sufficient to just catch and log, and prevent the stack trace from being in the logs
|
|
|
|
|
|
# but we need to confirm the root cause first.
|
|
|
|
|
|
current_app.logger.error(
|
|
|
|
|
|
f"create_beta_url orig_url: {url} \
|
|
|
|
|
|
url_created = {str(url_created)} url_for_redirect {str(url_for_redirect)}"
|
|
|
|
|
|
)
|
2023-09-14 14:59:48 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def redirect_notify_to_beta():
|
2023-09-18 06:24:17 -06:00
|
|
|
|
if (
|
|
|
|
|
|
current_app.config["NOTIFY_ENVIRONMENT"] == "production"
|
|
|
|
|
|
and "beta.notify.gov" not in request.url
|
|
|
|
|
|
):
|
2024-06-10 10:53:09 -07:00
|
|
|
|
# TODO add debug here to trace what is going on with the URL for the 'RequestContext' error
|
2023-09-18 06:24:17 -06:00
|
|
|
|
url_to_beta = create_beta_url(request.url)
|
2023-09-18 15:59:28 -04:00
|
|
|
|
return redirect(url_to_beta, 302)
|
2023-09-14 14:59:48 -06:00
|
|
|
|
|
|
|
|
|
|
|
2016-04-04 16:53:52 +01:00
|
|
|
|
def load_service_before_request():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if "/static/" in request.url:
|
2023-01-19 17:29:21 -05:00
|
|
|
|
request_ctx.service = None
|
2016-08-12 12:37:18 +01:00
|
|
|
|
return
|
2023-01-19 17:29:21 -05:00
|
|
|
|
if request_ctx is not None:
|
|
|
|
|
|
request_ctx.service = None
|
2017-10-30 16:59:24 +00:00
|
|
|
|
|
|
|
|
|
|
if request.view_args:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
service_id = request.view_args.get("service_id", session.get("service_id"))
|
2017-10-30 16:59:24 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
service_id = session.get("service_id")
|
2017-10-30 16:59:24 +00:00
|
|
|
|
|
|
|
|
|
|
if service_id:
|
|
|
|
|
|
try:
|
2023-01-19 17:29:21 -05:00
|
|
|
|
request_ctx.service = Service(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
service_api_client.get_service(service_id)["data"]
|
2018-10-26 08:20:00 +01:00
|
|
|
|
)
|
2024-02-01 11:37:31 -07:00
|
|
|
|
stats = service_api_client.get_service_statistics(
|
|
|
|
|
|
service_id, limit_days=7
|
|
|
|
|
|
)
|
|
|
|
|
|
request_ctx.service.stats = stats
|
2017-10-30 16:59:24 +00:00
|
|
|
|
except HTTPError as exc:
|
|
|
|
|
|
# if service id isn't real, then 404 rather than 500ing later because we expect service to be set
|
|
|
|
|
|
if exc.status_code == 404:
|
|
|
|
|
|
abort(404)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise
|
2016-04-04 16:53:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
2023-07-12 12:09:44 -04:00
|
|
|
|
def load_organization_before_request():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if "/static/" in request.url:
|
2023-07-12 12:09:44 -04:00
|
|
|
|
request_ctx.organization = None
|
2018-02-13 16:40:04 +00:00
|
|
|
|
return
|
2023-01-19 17:29:21 -05:00
|
|
|
|
if request_ctx is not None:
|
2023-07-12 12:09:44 -04:00
|
|
|
|
request_ctx.organization = None
|
2018-02-13 16:40:04 +00:00
|
|
|
|
|
|
|
|
|
|
if request.view_args:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
org_id = request.view_args.get("org_id")
|
2018-02-13 16:40:04 +00:00
|
|
|
|
|
2018-03-01 11:34:53 +00:00
|
|
|
|
if org_id:
|
|
|
|
|
|
try:
|
2023-07-12 12:09:44 -04:00
|
|
|
|
request_ctx.organization = Organization.from_id(org_id)
|
2018-03-01 11:34:53 +00:00
|
|
|
|
except HTTPError as exc:
|
|
|
|
|
|
# if org id isn't real, then 404 rather than 500ing later because we expect org to be set
|
|
|
|
|
|
if exc.status_code == 404:
|
|
|
|
|
|
abort(404)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise
|
2018-02-13 16:40:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
2018-03-07 18:30:26 +00:00
|
|
|
|
def save_service_or_org_after_request(response):
|
2016-04-04 16:53:52 +01:00
|
|
|
|
# Only save the current session if the request is 200
|
2023-08-25 09:12:23 -07:00
|
|
|
|
service_id = (
|
|
|
|
|
|
request.view_args.get("service_id", None) if request.view_args else None
|
|
|
|
|
|
)
|
|
|
|
|
|
organization_id = (
|
|
|
|
|
|
request.view_args.get("org_id", None) if request.view_args else None
|
|
|
|
|
|
)
|
2018-03-07 18:30:26 +00:00
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
if service_id:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["service_id"] = service_id
|
|
|
|
|
|
session["organization_id"] = None
|
2023-07-12 12:09:44 -04:00
|
|
|
|
elif organization_id:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["service_id"] = None
|
|
|
|
|
|
session["organization_id"] = organization_id
|
2016-04-04 16:53:52 +01:00
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-11-01 16:02:05 +00:00
|
|
|
|
def register_errorhandlers(application): # noqa (C901 too complex)
|
2020-02-20 17:46:48 +00:00
|
|
|
|
def _error_response(error_code, error_page_template=None):
|
|
|
|
|
|
if error_page_template is None:
|
|
|
|
|
|
error_page_template = error_code
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return make_response(
|
|
|
|
|
|
render_template("error/{0}.html".format(error_page_template)), error_code
|
|
|
|
|
|
)
|
2016-03-10 14:56:47 +00:00
|
|
|
|
|
2016-03-10 11:53:29 +00:00
|
|
|
|
@application.errorhandler(HTTPError)
|
|
|
|
|
|
def render_http_error(error):
|
2025-07-29 17:33:13 -04:00
|
|
|
|
error_url = error.response.url if error.response else "unknown URL"
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
application.logger.warning(
|
2025-07-29 17:33:13 -04:00
|
|
|
|
f"API {error_url} failed with status {error.status_code} message {error.message}",
|
|
|
|
|
|
exc_info=sys.exc_info(),
|
2025-07-31 13:38:50 -04:00
|
|
|
|
stack_info=True,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
)
|
2025-07-29 17:33:13 -04:00
|
|
|
|
|
2016-03-11 10:16:06 +00:00
|
|
|
|
error_code = error.status_code
|
2025-07-29 17:33:13 -04:00
|
|
|
|
|
2020-02-20 17:48:48 +00:00
|
|
|
|
if error_code not in [401, 404, 403, 410]:
|
|
|
|
|
|
# probably a 500 or 503.
|
|
|
|
|
|
# it might be a 400, which we should handle as if it's an internal server error. If the API might
|
|
|
|
|
|
# legitimately return a 400, we should handle that within the view or the client that calls it.
|
2023-08-25 09:12:23 -07:00
|
|
|
|
application.logger.exception(
|
2025-07-29 17:33:13 -04:00
|
|
|
|
f"API {error_url} failed with status {error.status_code} message {error.message}",
|
|
|
|
|
|
exc_info=sys.exc_info(),
|
2025-07-31 13:38:50 -04:00
|
|
|
|
stack_info=True,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
)
|
2025-07-29 17:33:13 -04:00
|
|
|
|
|
2016-03-10 11:53:29 +00:00
|
|
|
|
error_code = 500
|
2025-07-29 17:33:13 -04:00
|
|
|
|
|
2016-03-10 14:56:47 +00:00
|
|
|
|
return _error_response(error_code)
|
2016-10-10 11:36:12 +01:00
|
|
|
|
|
2018-05-30 13:50:29 +01:00
|
|
|
|
@application.errorhandler(400)
|
2020-02-20 17:46:48 +00:00
|
|
|
|
def handle_client_error(error):
|
|
|
|
|
|
# This is tripped if we call `abort(400)`.
|
2023-08-25 09:12:23 -07:00
|
|
|
|
application.logger.exception("Unhandled 400 client error")
|
2020-02-20 17:46:48 +00:00
|
|
|
|
return _error_response(400, error_page_template=500)
|
2018-05-30 13:50:29 +01:00
|
|
|
|
|
2016-10-10 11:36:12 +01:00
|
|
|
|
@application.errorhandler(410)
|
|
|
|
|
|
def handle_gone(error):
|
|
|
|
|
|
return _error_response(410)
|
2016-03-10 14:56:47 +00:00
|
|
|
|
|
|
|
|
|
|
@application.errorhandler(404)
|
|
|
|
|
|
def handle_not_found(error):
|
|
|
|
|
|
return _error_response(404)
|
|
|
|
|
|
|
|
|
|
|
|
@application.errorhandler(403)
|
|
|
|
|
|
def handle_not_authorized(error):
|
|
|
|
|
|
return _error_response(403)
|
|
|
|
|
|
|
|
|
|
|
|
@application.errorhandler(401)
|
|
|
|
|
|
def handle_no_permissions(error):
|
|
|
|
|
|
return _error_response(401)
|
|
|
|
|
|
|
2017-11-01 15:47:05 +00:00
|
|
|
|
@application.errorhandler(BadSignature)
|
|
|
|
|
|
def handle_bad_token(error):
|
|
|
|
|
|
# if someone has a malformed token
|
2023-08-25 09:12:23 -07:00
|
|
|
|
flash("There’s something wrong with the link you’ve used.")
|
2017-11-01 15:47:05 +00:00
|
|
|
|
return _error_response(404)
|
|
|
|
|
|
|
2017-11-28 12:11:29 +00:00
|
|
|
|
@application.errorhandler(CSRFError)
|
|
|
|
|
|
def handle_csrf(reason):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
application.logger.warning("csrf.error_message: {}".format(reason))
|
2017-11-28 12:11:29 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if "user_id" not in session:
|
2017-11-28 12:11:29 +00:00
|
|
|
|
application.logger.warning(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"csrf.session_expired: Redirecting user to log in page"
|
2017-11-28 12:11:29 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return application.login_manager.unauthorized()
|
|
|
|
|
|
|
|
|
|
|
|
application.logger.warning(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"csrf.invalid_token: Aborting request, user_id: {user_id}",
|
|
|
|
|
|
extra={"user_id": session["user_id"]},
|
|
|
|
|
|
)
|
2017-11-28 12:11:29 +00:00
|
|
|
|
|
2020-02-20 17:46:48 +00:00
|
|
|
|
return _error_response(400, error_page_template=500)
|
2017-11-28 12:11:29 +00:00
|
|
|
|
|
2018-06-22 17:36:58 +01:00
|
|
|
|
@application.errorhandler(405)
|
2020-02-20 17:46:48 +00:00
|
|
|
|
def handle_method_not_allowed(error):
|
|
|
|
|
|
return _error_response(405, error_page_template=500)
|
2018-06-22 17:36:58 +01:00
|
|
|
|
|
|
|
|
|
|
@application.errorhandler(WerkzeugHTTPException)
|
|
|
|
|
|
def handle_http_error(error):
|
|
|
|
|
|
if error.code == 301:
|
2019-03-21 16:41:22 +00:00
|
|
|
|
# PermanentRedirect exception
|
2018-06-22 17:36:58 +01:00
|
|
|
|
return error
|
|
|
|
|
|
|
|
|
|
|
|
return _error_response(error.code)
|
2018-05-30 14:54:25 +01:00
|
|
|
|
|
2020-03-06 11:53:55 +00:00
|
|
|
|
@application.errorhandler(InviteTokenError)
|
|
|
|
|
|
def handle_bad_invite_token(error):
|
|
|
|
|
|
flash(str(error))
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return redirect(url_for("main.sign_in"))
|
2020-03-06 11:53:55 +00:00
|
|
|
|
|
2018-02-12 16:02:33 +00:00
|
|
|
|
@application.errorhandler(500)
|
|
|
|
|
|
@application.errorhandler(Exception)
|
|
|
|
|
|
def handle_bad_request(error):
|
|
|
|
|
|
current_app.logger.exception(error)
|
|
|
|
|
|
# We want the Flask in browser stacktrace
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if current_app.config.get("DEBUG", None):
|
2018-02-12 16:02:33 +00:00
|
|
|
|
raise error
|
|
|
|
|
|
return _error_response(500)
|
|
|
|
|
|
|
2016-04-27 16:39:17 +01:00
|
|
|
|
|
|
|
|
|
|
def setup_event_handlers():
|
2017-07-26 11:02:57 +01:00
|
|
|
|
from flask_login import user_logged_in
|
2021-03-08 15:36:23 +00:00
|
|
|
|
|
2017-02-23 16:43:09 +00:00
|
|
|
|
from app.event_handlers import on_user_logged_in
|
2016-04-27 16:39:17 +01:00
|
|
|
|
|
|
|
|
|
|
user_logged_in.connect(on_user_logged_in)
|
2017-11-06 13:07:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_template_filters(application):
|
2025-02-24 16:11:42 -05:00
|
|
|
|
application.add_template_filter(slugify)
|
|
|
|
|
|
|
2017-11-09 15:54:49 +00:00
|
|
|
|
for fn in [
|
2022-03-14 14:39:54 +00:00
|
|
|
|
format_auth_type,
|
2021-03-11 13:23:02 +00:00
|
|
|
|
format_billions,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
format_datetime,
|
|
|
|
|
|
format_datetime_24h,
|
|
|
|
|
|
format_datetime_normal,
|
2024-04-09 12:52:45 -07:00
|
|
|
|
format_datetime_scheduled_notification,
|
2024-03-18 15:10:26 -07:00
|
|
|
|
format_datetime_table,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
valid_phone_number,
|
|
|
|
|
|
linkable_name,
|
|
|
|
|
|
format_date,
|
2019-10-18 16:43:52 +01:00
|
|
|
|
format_date_human,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
format_date_normal,
|
2020-05-11 10:52:30 +01:00
|
|
|
|
format_date_numeric,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
format_date_short,
|
2020-02-19 17:11:31 +00:00
|
|
|
|
format_datetime_human,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
format_datetime_relative,
|
2020-02-17 12:58:05 +00:00
|
|
|
|
format_day_of_week,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
format_delta,
|
2020-02-12 14:19:21 +00:00
|
|
|
|
format_delta_days,
|
2023-06-26 14:07:28 -07:00
|
|
|
|
format_time_24h,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
format_notification_status,
|
2018-07-17 14:39:04 +01:00
|
|
|
|
format_notification_type,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
format_notification_status_as_time,
|
|
|
|
|
|
format_notification_status_as_field_status,
|
|
|
|
|
|
format_notification_status_as_url,
|
2020-07-14 14:45:37 +01:00
|
|
|
|
format_number_in_pounds_as_currency,
|
2021-01-06 12:12:01 +00:00
|
|
|
|
formatted_list,
|
2021-10-29 16:10:55 +01:00
|
|
|
|
get_lines_with_normalised_whitespace,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
nl2br,
|
|
|
|
|
|
format_phone_number_human_readable,
|
2019-07-08 12:00:40 +01:00
|
|
|
|
format_thousands,
|
2018-09-21 14:24:31 +01:00
|
|
|
|
id_safe,
|
2019-11-14 16:22:06 +00:00
|
|
|
|
convert_to_boolean,
|
2025-06-25 13:25:01 -07:00
|
|
|
|
convert_time_unixtimestamp,
|
2020-12-17 10:46:42 +00:00
|
|
|
|
format_list_items,
|
2021-01-06 12:59:48 +00:00
|
|
|
|
iteration_count,
|
|
|
|
|
|
recipient_count,
|
|
|
|
|
|
recipient_count_label,
|
2021-06-24 16:54:08 +01:00
|
|
|
|
round_to_significant_figures,
|
2021-01-06 12:59:48 +00:00
|
|
|
|
message_count_label,
|
|
|
|
|
|
message_count,
|
|
|
|
|
|
message_count_noun,
|
2021-02-15 21:02:37 +00:00
|
|
|
|
format_mobile_network,
|
2021-10-11 14:26:46 +01:00
|
|
|
|
format_yes_no,
|
2021-08-03 18:46:30 +01:00
|
|
|
|
square_metres_to_square_miles,
|
2024-01-11 12:43:49 -05:00
|
|
|
|
convert_markdown_template,
|
2017-11-09 15:54:49 +00:00
|
|
|
|
]:
|
|
|
|
|
|
application.add_template_filter(fn)
|
2019-10-03 13:00:32 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_jinja(application):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
2019-10-03 13:00:32 +01:00
|
|
|
|
template_folders = [
|
2023-08-25 09:12:23 -07:00
|
|
|
|
os.path.join(repo_root, "app/templates"),
|
2019-10-03 13:00:32 +01:00
|
|
|
|
]
|
|
|
|
|
|
jinja_loader = jinja2.FileSystemLoader(template_folders)
|
|
|
|
|
|
application.jinja_loader = jinja_loader
|
2025-02-24 16:11:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def slugify(text):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Converts text to lowercase, replaces spaces with hyphens, and removes invalid characters.
|
|
|
|
|
|
"""
|
2025-03-17 16:49:30 -04:00
|
|
|
|
return re.sub(r"[^a-z0-9-]", "", re.sub(r"\s+", "-", text.lower()))
|