2019-03-25 10:25:05 +00:00
|
|
|
|
import itertools
|
2015-11-24 09:40:14 +00:00
|
|
|
|
import os
|
2016-03-17 10:59:47 +00:00
|
|
|
|
import urllib
|
2016-09-21 10:13:25 +01:00
|
|
|
|
from datetime import datetime, timedelta, timezone
|
2019-03-25 10:25:05 +00:00
|
|
|
|
from functools import partial
|
2016-10-19 11:54:51 +01:00
|
|
|
|
from time import monotonic
|
|
|
|
|
|
|
|
|
|
|
|
import ago
|
2019-10-03 13:00:32 +01:00
|
|
|
|
import jinja2
|
2016-04-04 16:53:52 +01:00
|
|
|
|
from flask import (
|
|
|
|
|
|
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,
|
|
|
|
|
|
render_template,
|
|
|
|
|
|
request,
|
|
|
|
|
|
session,
|
2017-11-01 15:47:05 +00:00
|
|
|
|
url_for,
|
2017-10-18 14:51:26 +01:00
|
|
|
|
)
|
2015-11-24 09:40:14 +00:00
|
|
|
|
from flask._compat import string_types
|
2017-05-04 11:28:45 +01:00
|
|
|
|
from flask.globals import _lookup_req_object, _request_ctx_stack
|
2018-01-25 15:29:05 +00:00
|
|
|
|
from flask_login import LoginManager, current_user
|
2017-07-27 15:17:17 +01:00
|
|
|
|
from flask_wtf import CSRFProtect
|
2017-07-26 11:01:24 +01:00
|
|
|
|
from flask_wtf.csrf import CSRFError
|
2019-10-03 13:00:32 +01:00
|
|
|
|
from govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
2019-03-25 10:25:05 +00:00
|
|
|
|
from itsdangerous import BadSignature
|
2016-04-15 11:04:35 +01:00
|
|
|
|
from notifications_python_client.errors import HTTPError
|
2019-03-25 10:25:05 +00:00
|
|
|
|
from notifications_utils import formatters, logging, request_helper
|
|
|
|
|
|
from notifications_utils.formatters import formatted_list
|
2017-06-06 12:57:55 +01:00
|
|
|
|
from notifications_utils.recipients import (
|
|
|
|
|
|
InvalidPhoneError,
|
|
|
|
|
|
format_phone_number_human_readable,
|
2019-03-25 10:25:05 +00:00
|
|
|
|
validate_phone_number,
|
2017-06-06 12:57:55 +01:00
|
|
|
|
)
|
2018-05-25 10:18:39 +01:00
|
|
|
|
from notifications_utils.sanitise_text import SanitiseASCII
|
2018-11-26 14:43:42 +00:00
|
|
|
|
from notifications_utils.timezones import utc_string_to_aware_gmt_datetime
|
2019-03-25 10:25:05 +00:00
|
|
|
|
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
|
|
|
|
|
|
from werkzeug.exceptions import abort
|
2016-04-21 09:30:33 +01:00
|
|
|
|
from werkzeug.local import LocalProxy
|
2016-03-10 14:56:47 +00:00
|
|
|
|
|
2017-10-18 14:51:26 +01:00
|
|
|
|
from app import proxy_fix
|
2018-10-26 15:39:32 +01:00
|
|
|
|
from app.asset_fingerprinter import asset_fingerprinter
|
2019-03-25 10:25:05 +00:00
|
|
|
|
from app.commands import setup_commands
|
|
|
|
|
|
from app.config import configs
|
2019-02-14 14:25:31 +00:00
|
|
|
|
from app.extensions import (
|
|
|
|
|
|
antivirus_client,
|
2019-03-25 10:25:05 +00:00
|
|
|
|
redis_client,
|
2019-02-14 14:25:31 +00:00
|
|
|
|
statsd_client,
|
|
|
|
|
|
zendesk_client,
|
|
|
|
|
|
)
|
2019-05-23 17:17:26 +01:00
|
|
|
|
from app.models.organisation import Organisation
|
2018-10-26 15:58:44 +01:00
|
|
|
|
from app.models.service import Service
|
2019-05-23 15:27:35 +01:00
|
|
|
|
from app.models.user import AnonymousUser, User
|
2018-06-12 16:17:20 +01:00
|
|
|
|
from app.navigation import (
|
|
|
|
|
|
CaseworkNavigation,
|
|
|
|
|
|
HeaderNavigation,
|
|
|
|
|
|
MainNavigation,
|
2019-03-25 10:25:05 +00:00
|
|
|
|
OrgNavigation,
|
2018-06-12 16:17:20 +01:00
|
|
|
|
)
|
2018-10-26 15:39:32 +01:00
|
|
|
|
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
|
2019-03-25 10:25:05 +00:00
|
|
|
|
from app.notify_client.email_branding_client import email_branding_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.letter_branding_client import letter_branding_client
|
|
|
|
|
|
from app.notify_client.letter_jobs_client import letter_jobs_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.organisations_api_client import organisations_client
|
|
|
|
|
|
from app.notify_client.platform_stats_api_client import (
|
|
|
|
|
|
platform_stats_api_client,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.notify_client.provider_client import provider_client
|
|
|
|
|
|
from app.notify_client.service_api_client import service_api_client
|
|
|
|
|
|
from app.notify_client.status_api_client import status_api_client
|
|
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.notify_client.user_api_client import user_api_client
|
2019-11-04 11:20:08 +00:00
|
|
|
|
from app.url_converters import (
|
|
|
|
|
|
LetterFileExtensionConverter,
|
|
|
|
|
|
TemplateTypeConverter,
|
|
|
|
|
|
)
|
2019-10-18 16:09:39 +01:00
|
|
|
|
from app.utils import format_thousands, get_logo_cdn_domain, id_safe
|
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()
|
2018-10-26 15:29:59 +01:00
|
|
|
|
|
2018-07-20 08:43:02 +01:00
|
|
|
|
|
2016-04-04 16:53:52 +01:00
|
|
|
|
# The current service attached to the request stack.
|
2018-07-20 08:43:02 +01:00
|
|
|
|
def _get_current_service():
|
2018-10-26 08:20:00 +01:00
|
|
|
|
return _lookup_req_object('service')
|
2018-07-20 08:43:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current_service = LocalProxy(_get_current_service)
|
2016-04-04 16:53:52 +01:00
|
|
|
|
|
2018-02-13 14:49:03 +00:00
|
|
|
|
# The current organisation attached to the request stack.
|
|
|
|
|
|
current_organisation = LocalProxy(partial(_lookup_req_object, 'organisation'))
|
2015-11-20 16:22:44 +00:00
|
|
|
|
|
2018-04-25 11:11:37 +01:00
|
|
|
|
navigation = {
|
2018-06-12 16:17:20 +01:00
|
|
|
|
'casework_navigation': CaseworkNavigation(),
|
2018-04-25 11:11:37 +01:00
|
|
|
|
'main_navigation': MainNavigation(),
|
|
|
|
|
|
'header_navigation': HeaderNavigation(),
|
2018-04-25 13:17:47 +01:00
|
|
|
|
'org_navigation': OrgNavigation(),
|
2018-04-25 11:11:37 +01:00
|
|
|
|
}
|
2018-04-25 10:24:32 +01:00
|
|
|
|
|
2018-02-14 13:08:44 +00:00
|
|
|
|
|
2017-11-06 13:07:21 +00:00
|
|
|
|
def create_app(application):
|
|
|
|
|
|
setup_commands(application)
|
2015-11-20 16:22:44 +00:00
|
|
|
|
|
2017-01-09 19:14:04 +00:00
|
|
|
|
notify_environment = os.environ['NOTIFY_ENVIRONMENT']
|
2016-12-08 16:50:37 +00:00
|
|
|
|
|
|
|
|
|
|
application.config.from_object(configs[notify_environment])
|
2018-11-29 13:06:10 +00: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
|
|
|
|
|
2019-10-03 13:00:32 +01:00
|
|
|
|
init_govuk_frontend(application)
|
|
|
|
|
|
init_jinja(application)
|
|
|
|
|
|
|
2019-01-29 11:18:08 +00:00
|
|
|
|
for client in (
|
2019-01-30 13:45:05 +00:00
|
|
|
|
|
|
|
|
|
|
# Gubbins
|
2019-01-29 11:18:08 +00:00
|
|
|
|
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,
|
|
|
|
|
|
email_branding_client,
|
|
|
|
|
|
events_api_client,
|
|
|
|
|
|
inbound_number_client,
|
|
|
|
|
|
invite_api_client,
|
|
|
|
|
|
job_api_client,
|
|
|
|
|
|
letter_branding_client,
|
|
|
|
|
|
letter_jobs_client,
|
|
|
|
|
|
notification_api_client,
|
|
|
|
|
|
org_invite_api_client,
|
|
|
|
|
|
organisations_client,
|
|
|
|
|
|
platform_stats_api_client,
|
|
|
|
|
|
provider_client,
|
|
|
|
|
|
service_api_client,
|
|
|
|
|
|
status_api_client,
|
|
|
|
|
|
template_folder_api_client,
|
|
|
|
|
|
template_statistics_client,
|
|
|
|
|
|
user_api_client,
|
|
|
|
|
|
|
|
|
|
|
|
# External API clients
|
2019-02-14 14:25:31 +00:00
|
|
|
|
antivirus_client,
|
2019-01-30 13:45:05 +00:00
|
|
|
|
statsd_client,
|
|
|
|
|
|
zendesk_client,
|
2019-02-14 14:25:31 +00:00
|
|
|
|
redis_client
|
2019-01-30 13:45:05 +00:00
|
|
|
|
|
2019-01-29 11:18:08 +00:00
|
|
|
|
):
|
|
|
|
|
|
client.init_app(application)
|
|
|
|
|
|
|
2016-12-01 17:21:06 +00:00
|
|
|
|
logging.init_app(application, statsd_client)
|
2015-11-30 11:21:51 +00:00
|
|
|
|
|
2016-01-06 17:17:02 +00:00
|
|
|
|
login_manager.login_view = 'main.sign_in'
|
2016-03-02 16:29:39 +00:00
|
|
|
|
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
|
2015-11-24 09:40:14 +00:00
|
|
|
|
|
2019-02-14 14:25:31 +00:00
|
|
|
|
# make sure we handle unicode correctly
|
|
|
|
|
|
redis_client.redis_store.decode_responses = True
|
|
|
|
|
|
|
2015-11-20 16:33:11 +00:00
|
|
|
|
from app.main import main as main_blueprint
|
2015-11-20 16:22:44 +00:00
|
|
|
|
application.register_blueprint(main_blueprint)
|
|
|
|
|
|
|
2016-01-11 14:54:23 +00:00
|
|
|
|
from .status import status as status_blueprint
|
|
|
|
|
|
application.register_blueprint(status_blueprint)
|
|
|
|
|
|
|
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):
|
2017-11-06 13:07:21 +00:00
|
|
|
|
application.after_request(useful_headers_after_request)
|
2018-03-07 18:30:26 +00:00
|
|
|
|
application.after_request(save_service_or_org_after_request)
|
2017-11-06 13:07:21 +00:00
|
|
|
|
application.before_request(load_service_before_request)
|
2018-02-13 16:40:04 +00:00
|
|
|
|
application.before_request(load_organisation_before_request)
|
2017-11-16 16:25:40 +00:00
|
|
|
|
application.before_request(request_helper.check_proxy_header_before_request)
|
2017-11-06 13:07:21 +00:00
|
|
|
|
|
2018-01-05 16:27:51 +00:00
|
|
|
|
@application.before_request
|
|
|
|
|
|
def make_session_permanent():
|
|
|
|
|
|
# this is dumb. You'd think, given that there's `config['PERMANENT_SESSION_LIFETIME']`, that you'd enable
|
|
|
|
|
|
# permanent sessions in the config too - but no, you have to declare it for each request.
|
|
|
|
|
|
# https://stackoverflow.com/questions/34118093/flask-permanent-session-where-to-define-them
|
|
|
|
|
|
# session.permanent is also, helpfully, a way of saying that the session isn't permanent - in that, it will
|
|
|
|
|
|
# expire on its own, as opposed to being controlled by the browser's session. Because session is a proxy, it's
|
|
|
|
|
|
# only accessible from within a request context, so we need to set this before every request :rolls_eyes:
|
|
|
|
|
|
session.permanent = True
|
|
|
|
|
|
|
2017-11-06 13:07:21 +00:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _attach_current_service():
|
|
|
|
|
|
return {'current_service': current_service}
|
2016-05-12 13:56:14 +01:00
|
|
|
|
|
2018-02-13 14:49:03 +00:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _attach_current_organisation():
|
|
|
|
|
|
return {'current_org': current_organisation}
|
|
|
|
|
|
|
2018-01-25 15:29:05 +00:00
|
|
|
|
@application.context_processor
|
|
|
|
|
|
def _attach_current_user():
|
2019-03-28 16:07:26 +00:00
|
|
|
|
return {'current_user': current_user}
|
2018-01-25 15:29:05 +00:00
|
|
|
|
|
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
|
|
|
|
|
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 {
|
2018-11-28 15:39:08 +00:00
|
|
|
|
'asset_path': application.config['ASSET_PATH'],
|
2016-03-17 13:45:59 +00:00
|
|
|
|
'header_colour': application.config['HEADER_COLOUR'],
|
2016-02-10 15:47:00 +00:00
|
|
|
|
'asset_url': asset_fingerprinter.get_url
|
2016-02-01 14:46:12 +00:00
|
|
|
|
}
|
2015-12-15 08:20:25 +00:00
|
|
|
|
|
2019-11-04 12:20:09 +00:00
|
|
|
|
application.url_map.converters['uuid'].to_python = lambda self, value: value
|
2019-11-04 11:20:08 +00:00
|
|
|
|
application.url_map.converters['template_type'] = TemplateTypeConverter
|
|
|
|
|
|
application.url_map.converters['letter_file_extension'] = LetterFileExtensionConverter
|
2019-11-04 12:20:09 +00:00
|
|
|
|
|
2015-11-25 15:29:12 +00:00
|
|
|
|
|
2015-11-24 09:40:14 +00:00
|
|
|
|
def convert_to_boolean(value):
|
|
|
|
|
|
if isinstance(value, string_types):
|
|
|
|
|
|
if value.lower() in ['t', 'true', 'on', 'yes', '1']:
|
|
|
|
|
|
return True
|
|
|
|
|
|
elif value.lower() in ['f', 'false', 'off', 'no', '0']:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
return value
|
2015-12-10 16:26:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
2016-03-17 10:59:47 +00:00
|
|
|
|
def linkable_name(value):
|
|
|
|
|
|
return urllib.parse.quote_plus(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-05-17 14:00:40 +01:00
|
|
|
|
def format_datetime(date):
|
2016-08-26 08:06:28 +01:00
|
|
|
|
return '{} at {}'.format(
|
|
|
|
|
|
format_date(date),
|
|
|
|
|
|
format_time(date)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_datetime_24h(date):
|
|
|
|
|
|
return '{} at {}'.format(
|
|
|
|
|
|
format_date(date),
|
2017-10-17 11:41:12 +01:00
|
|
|
|
format_time_24h(date),
|
2016-08-26 08:06:28 +01:00
|
|
|
|
)
|
2016-01-21 12:28:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
2016-05-20 13:58:16 +01:00
|
|
|
|
def format_datetime_normal(date):
|
2016-08-26 08:06:28 +01:00
|
|
|
|
return '{} at {}'.format(
|
|
|
|
|
|
format_date_normal(date),
|
|
|
|
|
|
format_time(date)
|
|
|
|
|
|
)
|
2016-05-20 13:58:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-04-14 13:35:56 +01:00
|
|
|
|
def format_datetime_short(date):
|
2016-08-26 08:06:28 +01:00
|
|
|
|
return '{} at {}'.format(
|
|
|
|
|
|
format_date_short(date),
|
|
|
|
|
|
format_time(date)
|
|
|
|
|
|
)
|
2016-04-14 13:35:56 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-10-12 09:20:32 +01:00
|
|
|
|
def format_datetime_relative(date):
|
|
|
|
|
|
return '{} at {}'.format(
|
|
|
|
|
|
get_human_day(date),
|
|
|
|
|
|
format_time(date)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-10-17 11:41:12 +01:00
|
|
|
|
def format_datetime_numeric(date):
|
|
|
|
|
|
return '{} {}'.format(
|
|
|
|
|
|
format_date_numeric(date),
|
|
|
|
|
|
format_time_24h(date),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_date_numeric(date):
|
2018-11-26 14:43:42 +00:00
|
|
|
|
return utc_string_to_aware_gmt_datetime(date).strftime('%Y-%m-%d')
|
2017-10-17 11:41:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_time_24h(date):
|
2018-11-26 14:43:42 +00:00
|
|
|
|
return utc_string_to_aware_gmt_datetime(date).strftime('%H:%M')
|
2017-10-17 11:41:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-10-12 09:20:32 +01:00
|
|
|
|
def get_human_day(time):
|
2017-05-24 13:19:31 +01:00
|
|
|
|
|
2018-03-21 16:01:26 +00:00
|
|
|
|
# Add 1 minute to transform 00:00 into ‘midnight today’ instead of ‘midnight tomorrow’
|
2018-11-26 14:43:42 +00:00
|
|
|
|
date = (utc_string_to_aware_gmt_datetime(time) - timedelta(minutes=1)).date()
|
2019-10-21 14:14:59 +01:00
|
|
|
|
now = datetime.utcnow()
|
|
|
|
|
|
|
|
|
|
|
|
if date == (now + timedelta(days=1)).date():
|
2016-10-12 09:20:32 +01:00
|
|
|
|
return 'tomorrow'
|
2019-10-21 14:14:59 +01:00
|
|
|
|
if date == now.date():
|
2017-05-24 13:19:31 +01:00
|
|
|
|
return 'today'
|
2019-10-21 14:14:59 +01:00
|
|
|
|
if date == (now - timedelta(days=1)).date():
|
2017-05-24 13:19:31 +01:00
|
|
|
|
return 'yesterday'
|
2019-10-21 14:14:59 +01:00
|
|
|
|
if date.strftime('%Y') != now.strftime('%Y'):
|
|
|
|
|
|
return '{} {}'.format(_format_datetime_short(date), date.strftime('%Y'))
|
2018-03-21 16:01:26 +00:00
|
|
|
|
return _format_datetime_short(date)
|
2016-10-12 09:20:32 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-03-02 16:15:15 +00:00
|
|
|
|
def format_time(date):
|
2016-08-26 08:06:28 +01:00
|
|
|
|
return {
|
|
|
|
|
|
'12:00AM': 'Midnight',
|
|
|
|
|
|
'12:00PM': 'Midday'
|
|
|
|
|
|
}.get(
|
2018-11-26 14:43:42 +00:00
|
|
|
|
utc_string_to_aware_gmt_datetime(date).strftime('%-I:%M%p'),
|
|
|
|
|
|
utc_string_to_aware_gmt_datetime(date).strftime('%-I:%M%p')
|
2016-08-26 08:06:28 +01:00
|
|
|
|
).lower()
|
2016-03-02 16:15:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
2016-04-05 11:40:13 +01:00
|
|
|
|
def format_date(date):
|
2018-11-26 14:43:42 +00:00
|
|
|
|
return utc_string_to_aware_gmt_datetime(date).strftime('%A %d %B %Y')
|
2016-04-05 11:40:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-05-20 13:58:16 +01:00
|
|
|
|
def format_date_normal(date):
|
2018-11-26 14:43:42 +00:00
|
|
|
|
return utc_string_to_aware_gmt_datetime(date).strftime('%d %B %Y').lstrip('0')
|
2016-05-20 13:58:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-05-03 13:25:22 +01:00
|
|
|
|
def format_date_short(date):
|
2018-11-26 14:43:42 +00:00
|
|
|
|
return _format_datetime_short(utc_string_to_aware_gmt_datetime(date))
|
2018-03-21 16:01:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
2019-10-18 16:43:52 +01:00
|
|
|
|
def format_date_human(date):
|
|
|
|
|
|
return get_human_day(date)
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-03-21 16:01:26 +00:00
|
|
|
|
def _format_datetime_short(datetime):
|
|
|
|
|
|
return datetime.strftime('%d %B').lstrip('0')
|
2016-05-03 13:25:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-09-21 10:13:25 +01:00
|
|
|
|
def format_delta(date):
|
2017-04-06 11:03:05 +01:00
|
|
|
|
delta = (
|
|
|
|
|
|
datetime.now(timezone.utc)
|
|
|
|
|
|
) - (
|
2018-11-26 14:43:42 +00:00
|
|
|
|
utc_string_to_aware_gmt_datetime(date)
|
2017-04-06 11:03:05 +01:00
|
|
|
|
)
|
|
|
|
|
|
if delta < timedelta(seconds=30):
|
|
|
|
|
|
return "just now"
|
2017-06-26 15:41:08 +01:00
|
|
|
|
if delta < timedelta(seconds=60):
|
|
|
|
|
|
return "in the last minute"
|
2016-09-21 10:13:25 +01:00
|
|
|
|
return ago.human(
|
2017-04-06 11:03:05 +01:00
|
|
|
|
delta,
|
2016-09-21 10:13:25 +01:00
|
|
|
|
future_tense='{} from now', # No-one should ever see this
|
|
|
|
|
|
past_tense='{} ago',
|
|
|
|
|
|
precision=1
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-02-17 15:49:07 +00:00
|
|
|
|
def valid_phone_number(phone_number):
|
|
|
|
|
|
try:
|
|
|
|
|
|
validate_phone_number(phone_number)
|
|
|
|
|
|
return True
|
|
|
|
|
|
except InvalidPhoneError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-07-17 14:39:04 +01:00
|
|
|
|
def format_notification_type(notification_type):
|
|
|
|
|
|
return {
|
|
|
|
|
|
'email': 'Email',
|
|
|
|
|
|
'sms': 'SMS',
|
|
|
|
|
|
'letter': 'Letter'
|
|
|
|
|
|
}[notification_type]
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-06-07 16:35:03 +01:00
|
|
|
|
def format_notification_status(status, template_type):
|
|
|
|
|
|
return {
|
|
|
|
|
|
'email': {
|
|
|
|
|
|
'failed': 'Failed',
|
|
|
|
|
|
'technical-failure': 'Technical failure',
|
2016-07-11 10:45:54 +01:00
|
|
|
|
'temporary-failure': 'Inbox not accepting messages right now',
|
2019-09-13 12:12:49 +01:00
|
|
|
|
'permanent-failure': 'Email address does not exist',
|
2016-06-07 16:35:03 +01:00
|
|
|
|
'delivered': 'Delivered',
|
2016-08-03 09:47:12 +01:00
|
|
|
|
'sending': 'Sending',
|
2017-04-27 16:02:49 +01:00
|
|
|
|
'created': 'Sending',
|
|
|
|
|
|
'sent': 'Delivered'
|
2016-06-07 16:35:03 +01:00
|
|
|
|
},
|
|
|
|
|
|
'sms': {
|
|
|
|
|
|
'failed': 'Failed',
|
|
|
|
|
|
'technical-failure': 'Technical failure',
|
2016-07-11 10:45:54 +01:00
|
|
|
|
'temporary-failure': 'Phone not accepting messages right now',
|
2019-09-13 12:12:49 +01:00
|
|
|
|
'permanent-failure': 'Phone number does not exist',
|
2016-06-07 16:35:03 +01:00
|
|
|
|
'delivered': 'Delivered',
|
2016-08-03 09:47:12 +01:00
|
|
|
|
'sending': 'Sending',
|
2017-04-27 16:02:49 +01:00
|
|
|
|
'created': 'Sending',
|
2018-09-11 13:46:33 +01:00
|
|
|
|
'pending': 'Sending',
|
2017-04-27 16:02:49 +01:00
|
|
|
|
'sent': 'Sent internationally'
|
2017-03-20 16:25:07 +00:00
|
|
|
|
},
|
|
|
|
|
|
'letter': {
|
2019-01-10 17:23:03 +00:00
|
|
|
|
'failed': '',
|
2017-03-20 16:25:07 +00:00
|
|
|
|
'technical-failure': 'Technical failure',
|
2019-01-10 17:23:03 +00:00
|
|
|
|
'temporary-failure': '',
|
2019-01-14 09:57:36 +00:00
|
|
|
|
'permanent-failure': '',
|
2019-01-10 17:23:03 +00:00
|
|
|
|
'delivered': '',
|
|
|
|
|
|
'received': '',
|
|
|
|
|
|
'accepted': '',
|
|
|
|
|
|
'sending': '',
|
|
|
|
|
|
'created': '',
|
|
|
|
|
|
'sent': '',
|
|
|
|
|
|
'pending-virus-check': '',
|
2018-03-19 15:25:26 +00:00
|
|
|
|
'virus-scan-failed': 'Virus detected',
|
2019-01-10 17:23:03 +00:00
|
|
|
|
'returned-letter': '',
|
|
|
|
|
|
'cancelled': '',
|
2019-01-07 12:02:24 +00:00
|
|
|
|
'validation-failed': 'Validation failed',
|
2016-06-07 16:35:03 +01:00
|
|
|
|
}
|
2017-01-13 11:35:27 +00:00
|
|
|
|
}[template_type].get(status, status)
|
2016-05-18 09:54:50 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-09-09 15:57:05 +01:00
|
|
|
|
def format_notification_status_as_time(status, created, updated):
|
2018-09-11 13:46:33 +01:00
|
|
|
|
return dict.fromkeys(
|
|
|
|
|
|
{'created', 'pending', 'sending'}, ' since {}'.format(created)
|
|
|
|
|
|
).get(status, updated)
|
2016-09-09 12:25:39 +01:00
|
|
|
|
|
|
|
|
|
|
|
2018-03-06 13:51:50 +00:00
|
|
|
|
def format_notification_status_as_field_status(status, notification_type):
|
2016-06-09 10:15:37 +01:00
|
|
|
|
return {
|
2018-03-06 13:51:50 +00:00
|
|
|
|
'letter': {
|
|
|
|
|
|
'failed': 'error',
|
|
|
|
|
|
'technical-failure': 'error',
|
|
|
|
|
|
'temporary-failure': 'error',
|
|
|
|
|
|
'permanent-failure': 'error',
|
|
|
|
|
|
'delivered': None,
|
|
|
|
|
|
'sent': None,
|
|
|
|
|
|
'sending': None,
|
|
|
|
|
|
'created': None,
|
|
|
|
|
|
'accepted': None,
|
2018-03-19 15:25:26 +00:00
|
|
|
|
'pending-virus-check': None,
|
|
|
|
|
|
'virus-scan-failed': 'error',
|
2018-09-06 16:34:23 +01:00
|
|
|
|
'returned-letter': None,
|
2018-12-04 15:07:20 +00:00
|
|
|
|
'cancelled': 'error',
|
2018-03-06 13:51:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
}.get(
|
|
|
|
|
|
notification_type,
|
|
|
|
|
|
{
|
|
|
|
|
|
'failed': 'error',
|
|
|
|
|
|
'technical-failure': 'error',
|
|
|
|
|
|
'temporary-failure': 'error',
|
|
|
|
|
|
'permanent-failure': 'error',
|
|
|
|
|
|
'delivered': None,
|
|
|
|
|
|
'sent': None,
|
|
|
|
|
|
'sending': 'default',
|
2018-09-11 13:46:33 +01:00
|
|
|
|
'created': 'default',
|
|
|
|
|
|
'pending': 'default',
|
2018-03-06 13:51:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
).get(status, 'error')
|
2016-06-09 10:15:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
2019-05-13 13:25:39 +01:00
|
|
|
|
def format_notification_status_as_url(status, notification_type):
|
2019-05-09 16:31:01 +01:00
|
|
|
|
url = partial(url_for, "main.message_status")
|
|
|
|
|
|
|
2019-05-13 13:25:39 +01:00
|
|
|
|
if status not in {
|
|
|
|
|
|
'technical-failure', 'temporary-failure', 'permanent-failure',
|
|
|
|
|
|
}:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2016-07-11 13:53:55 +01:00
|
|
|
|
return {
|
2019-05-09 16:31:01 +01:00
|
|
|
|
'email': url(_anchor='email-statuses'),
|
|
|
|
|
|
'sms': url(_anchor='sms-statuses')
|
|
|
|
|
|
}.get(notification_type)
|
2016-07-11 13:53:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
2017-03-02 15:56:28 +00:00
|
|
|
|
def nl2br(value):
|
|
|
|
|
|
return formatters.nl2br(value) if value else ''
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2016-04-04 16:53:52 +01:00
|
|
|
|
def load_service_before_request():
|
2016-08-12 12:37:18 +01:00
|
|
|
|
if '/static/' in request.url:
|
2017-05-04 11:28:45 +01:00
|
|
|
|
_request_ctx_stack.top.service = None
|
2016-08-12 12:37:18 +01:00
|
|
|
|
return
|
2016-04-13 16:19:34 +01:00
|
|
|
|
if _request_ctx_stack.top is not None:
|
2017-10-30 16:59:24 +00:00
|
|
|
|
_request_ctx_stack.top.service = None
|
|
|
|
|
|
|
|
|
|
|
|
if request.view_args:
|
|
|
|
|
|
service_id = request.view_args.get('service_id', session.get('service_id'))
|
|
|
|
|
|
else:
|
|
|
|
|
|
service_id = session.get('service_id')
|
|
|
|
|
|
|
|
|
|
|
|
if service_id:
|
|
|
|
|
|
try:
|
2018-10-26 08:20:00 +01:00
|
|
|
|
_request_ctx_stack.top.service = Service(
|
|
|
|
|
|
service_api_client.get_service(service_id)['data']
|
|
|
|
|
|
)
|
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
|
|
|
|
|
|
|
|
|
|
|
2018-02-13 16:40:04 +00:00
|
|
|
|
def load_organisation_before_request():
|
|
|
|
|
|
if '/static/' in request.url:
|
|
|
|
|
|
_request_ctx_stack.top.organisation = None
|
|
|
|
|
|
return
|
|
|
|
|
|
if _request_ctx_stack.top is not None:
|
|
|
|
|
|
_request_ctx_stack.top.organisation = None
|
|
|
|
|
|
|
|
|
|
|
|
if request.view_args:
|
2018-03-01 11:34:53 +00: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:
|
2019-05-23 17:17:26 +01:00
|
|
|
|
_request_ctx_stack.top.organisation = Organisation.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
|
|
|
|
|
|
service_id = request.view_args.get('service_id', None) if request.view_args else None
|
2018-03-07 18:30:26 +00:00
|
|
|
|
organisation_id = request.view_args.get('org_id', None) if request.view_args else None
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
|
if service_id:
|
|
|
|
|
|
session['service_id'] = service_id
|
|
|
|
|
|
session['organisation_id'] = None
|
|
|
|
|
|
elif organisation_id:
|
|
|
|
|
|
session['service_id'] = None
|
|
|
|
|
|
session['organisation_id'] = organisation_id
|
2016-04-04 16:53:52 +01:00
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
2016-01-07 13:58:38 +00:00
|
|
|
|
def useful_headers_after_request(response):
|
|
|
|
|
|
response.headers.add('X-Frame-Options', 'deny')
|
|
|
|
|
|
response.headers.add('X-Content-Type-Options', 'nosniff')
|
|
|
|
|
|
response.headers.add('X-XSS-Protection', '1; mode=block')
|
2016-07-05 07:12:21 +01:00
|
|
|
|
response.headers.add('Content-Security-Policy', (
|
2018-11-29 11:41:13 +00:00
|
|
|
|
"default-src 'self' {asset_domain} 'unsafe-inline';"
|
|
|
|
|
|
"script-src 'self' {asset_domain} *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
|
2017-11-06 10:25:30 +00:00
|
|
|
|
"connect-src 'self' *.google-analytics.com;"
|
2016-07-05 07:12:21 +01:00
|
|
|
|
"object-src 'self';"
|
2018-11-29 11:41:13 +00:00
|
|
|
|
"font-src 'self' {asset_domain} data:;"
|
|
|
|
|
|
"img-src 'self' {asset_domain} *.google-analytics.com *.notifications.service.gov.uk {logo_domain} data:;"
|
2018-11-29 11:29:52 +00:00
|
|
|
|
"frame-src 'self' www.youtube.com;".format(
|
2018-11-29 12:07:48 +00:00
|
|
|
|
asset_domain=current_app.config['ASSET_DOMAIN'],
|
2018-11-29 11:41:13 +00:00
|
|
|
|
logo_domain=get_logo_cdn_domain(),
|
2018-11-29 11:29:52 +00:00
|
|
|
|
)
|
2016-07-05 07:12:21 +01:00
|
|
|
|
))
|
2016-02-02 14:02:10 +00:00
|
|
|
|
if 'Cache-Control' in response.headers:
|
|
|
|
|
|
del response.headers['Cache-Control']
|
|
|
|
|
|
response.headers.add(
|
2016-02-02 16:50:13 +00:00
|
|
|
|
'Cache-Control', 'no-store, no-cache, private, must-revalidate')
|
2018-05-25 10:18:39 +01:00
|
|
|
|
for key, value in response.headers:
|
|
|
|
|
|
response.headers[key] = SanitiseASCII.encode(value)
|
2016-01-07 13:58:38 +00:00
|
|
|
|
return response
|
2016-01-07 15:48:29 +00:00
|
|
|
|
|
2016-01-07 15:55:55 +00:00
|
|
|
|
|
2017-11-01 16:02:05 +00:00
|
|
|
|
def register_errorhandlers(application): # noqa (C901 too complex)
|
2016-03-10 14:56:47 +00:00
|
|
|
|
def _error_response(error_code):
|
|
|
|
|
|
resp = make_response(render_template("error/{0}.html".format(error_code)), error_code)
|
|
|
|
|
|
return useful_headers_after_request(resp)
|
|
|
|
|
|
|
2016-03-10 11:53:29 +00:00
|
|
|
|
@application.errorhandler(HTTPError)
|
|
|
|
|
|
def render_http_error(error):
|
2018-03-08 16:10:17 +00:00
|
|
|
|
application.logger.warning("API {} failed with status {} message {}".format(
|
2016-07-21 17:32:28 +01:00
|
|
|
|
error.response.url if error.response else 'unknown',
|
2016-07-19 13:53:27 +01:00
|
|
|
|
error.status_code,
|
|
|
|
|
|
error.message
|
|
|
|
|
|
))
|
2016-03-11 10:16:06 +00:00
|
|
|
|
error_code = error.status_code
|
2016-12-07 14:00:03 +00:00
|
|
|
|
if error_code == 400:
|
2017-06-29 12:22:34 +01:00
|
|
|
|
if isinstance(error.message, str):
|
|
|
|
|
|
msg = [error.message]
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = list(itertools.chain(*[error.message[x] for x in error.message.keys()]))
|
2016-12-07 14:00:03 +00:00
|
|
|
|
resp = make_response(render_template("error/400.html", message=msg))
|
|
|
|
|
|
return useful_headers_after_request(resp)
|
2018-03-08 16:10:17 +00:00
|
|
|
|
elif error_code not in [401, 404, 403, 410]:
|
|
|
|
|
|
# probably a 500 or 503
|
|
|
|
|
|
application.logger.exception("API {} failed with status {} message {}".format(
|
|
|
|
|
|
error.response.url if error.response else 'unknown',
|
|
|
|
|
|
error.status_code,
|
|
|
|
|
|
error.message
|
|
|
|
|
|
))
|
2016-03-10 11:53:29 +00:00
|
|
|
|
error_code = 500
|
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)
|
|
|
|
|
|
def handle_400(error):
|
|
|
|
|
|
return _error_response(400)
|
|
|
|
|
|
|
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
|
|
|
|
|
2018-03-08 17:49:08 +00:00
|
|
|
|
@application.errorhandler(413)
|
|
|
|
|
|
def handle_payload_too_large(error):
|
|
|
|
|
|
return _error_response(413)
|
|
|
|
|
|
|
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
|
|
|
|
|
|
flash('There’s something wrong with the link you’ve used.')
|
|
|
|
|
|
return _error_response(404)
|
|
|
|
|
|
|
2017-11-28 12:11:29 +00:00
|
|
|
|
@application.errorhandler(CSRFError)
|
|
|
|
|
|
def handle_csrf(reason):
|
|
|
|
|
|
application.logger.warning('csrf.error_message: {}'.format(reason))
|
|
|
|
|
|
|
|
|
|
|
|
if 'user_id' not in session:
|
|
|
|
|
|
application.logger.warning(
|
|
|
|
|
|
u'csrf.session_expired: Redirecting user to log in page'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return application.login_manager.unauthorized()
|
|
|
|
|
|
|
|
|
|
|
|
application.logger.warning(
|
|
|
|
|
|
u'csrf.invalid_token: Aborting request, user_id: {user_id}',
|
|
|
|
|
|
extra={'user_id': session['user_id']})
|
|
|
|
|
|
|
|
|
|
|
|
resp = make_response(render_template(
|
|
|
|
|
|
"error/400.html",
|
|
|
|
|
|
message=['Something went wrong, please go back and try again.']
|
|
|
|
|
|
), 400)
|
|
|
|
|
|
return useful_headers_after_request(resp)
|
|
|
|
|
|
|
2018-06-22 17:36:58 +01:00
|
|
|
|
@application.errorhandler(405)
|
|
|
|
|
|
def handle_405(error):
|
|
|
|
|
|
resp = make_response(render_template(
|
|
|
|
|
|
"error/400.html",
|
|
|
|
|
|
message=['Something went wrong, please go back and try again.']
|
|
|
|
|
|
), 405)
|
|
|
|
|
|
return useful_headers_after_request(resp)
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
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
|
|
|
|
|
|
if current_app.config.get('DEBUG', None):
|
|
|
|
|
|
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
|
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):
|
2017-11-09 15:54:49 +00:00
|
|
|
|
for fn in [
|
|
|
|
|
|
format_datetime,
|
|
|
|
|
|
format_datetime_24h,
|
|
|
|
|
|
format_datetime_normal,
|
|
|
|
|
|
format_datetime_short,
|
|
|
|
|
|
format_time,
|
|
|
|
|
|
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,
|
|
|
|
|
|
format_date_short,
|
|
|
|
|
|
format_datetime_relative,
|
|
|
|
|
|
format_delta,
|
|
|
|
|
|
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,
|
|
|
|
|
|
formatted_list,
|
|
|
|
|
|
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,
|
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):
|
|
|
|
|
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
|
|
template_folders = [
|
|
|
|
|
|
os.path.join(repo_root, 'app/templates'),
|
|
|
|
|
|
os.path.join(repo_root, 'node_modules/govuk-frontend'),
|
|
|
|
|
|
]
|
|
|
|
|
|
jinja_loader = jinja2.FileSystemLoader(template_folders)
|
|
|
|
|
|
application.jinja_loader = jinja_loader
|