mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-05 02:18:24 -04:00
notify-api-412 use black to enforce python coding style
This commit is contained in:
@@ -10,21 +10,20 @@ from app.utils.user import user_is_gov_user, user_is_logged_in
|
||||
|
||||
|
||||
def _create_service(service_name, organization_type, email_from, form):
|
||||
|
||||
try:
|
||||
service_id = service_api_client.create_service(
|
||||
service_name=service_name,
|
||||
organization_type=organization_type,
|
||||
message_limit=current_app.config['DEFAULT_SERVICE_LIMIT'],
|
||||
message_limit=current_app.config["DEFAULT_SERVICE_LIMIT"],
|
||||
restricted=True,
|
||||
user_id=session['user_id'],
|
||||
user_id=session["user_id"],
|
||||
email_from=email_from,
|
||||
)
|
||||
session['service_id'] = service_id
|
||||
session["service_id"] = service_id
|
||||
|
||||
return service_id, None
|
||||
except HTTPError as e:
|
||||
if e.status_code == 400 and e.message['name']:
|
||||
if e.status_code == 400 and e.message["name"]:
|
||||
form.name.errors.append("This service name is already in use")
|
||||
return None, e
|
||||
else:
|
||||
@@ -33,15 +32,15 @@ def _create_service(service_name, organization_type, email_from, form):
|
||||
|
||||
def _create_example_template(service_id):
|
||||
example_sms_template = service_api_client.create_service_template(
|
||||
'Example text message template',
|
||||
'sms',
|
||||
'Hi, I’m trying out Notify.gov. Today is ((day of week)) and my favorite color is ((color)).',
|
||||
"Example text message template",
|
||||
"sms",
|
||||
"Hi, I’m trying out Notify.gov. Today is ((day of week)) and my favorite color is ((color)).",
|
||||
service_id,
|
||||
)
|
||||
return example_sms_template
|
||||
|
||||
|
||||
@main.route("/add-service", methods=['GET', 'POST'])
|
||||
@main.route("/add-service", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
@user_is_gov_user
|
||||
def add_service():
|
||||
@@ -64,33 +63,42 @@ def add_service():
|
||||
)
|
||||
if error:
|
||||
return _render_add_service_page(form, default_organization_type)
|
||||
if len(service_api_client.get_active_services({'user_id': session['user_id']}).get('data', [])) > 1:
|
||||
return redirect(url_for('main.service_dashboard', service_id=service_id))
|
||||
if (
|
||||
len(
|
||||
service_api_client.get_active_services(
|
||||
{"user_id": session["user_id"]}
|
||||
).get("data", [])
|
||||
)
|
||||
> 1
|
||||
):
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
|
||||
example_sms_template = _create_example_template(service_id)
|
||||
|
||||
return redirect(url_for(
|
||||
'main.begin_tour',
|
||||
service_id=service_id,
|
||||
template_id=example_sms_template['data']['id']
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.begin_tour",
|
||||
service_id=service_id,
|
||||
template_id=example_sms_template["data"]["id"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
return _render_add_service_page(form, default_organization_type)
|
||||
|
||||
|
||||
def _render_add_service_page(form, default_organization_type):
|
||||
heading = 'About your service'
|
||||
heading = "About your service"
|
||||
|
||||
if default_organization_type == 'local':
|
||||
if default_organization_type == "local":
|
||||
return render_template(
|
||||
'views/add-service-local.html',
|
||||
"views/add-service-local.html",
|
||||
form=form,
|
||||
heading=heading,
|
||||
default_organization_type=default_organization_type,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/add-service.html',
|
||||
"views/add-service.html",
|
||||
form=form,
|
||||
heading=heading,
|
||||
default_organization_type=default_organization_type,
|
||||
|
||||
@@ -17,76 +17,84 @@ from app.notify_client.api_key_api_client import (
|
||||
)
|
||||
from app.utils.user import user_has_permissions
|
||||
|
||||
dummy_bearer_token = 'bearer_token_set' # nosec B105 - this is not a real token
|
||||
dummy_bearer_token = "bearer_token_set" # nosec B105 - this is not a real token
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api")
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def api_integration(service_id):
|
||||
callbacks_link = (
|
||||
'.api_callbacks' if current_service.has_permission('inbound_sms')
|
||||
else '.delivery_status_callback'
|
||||
".api_callbacks"
|
||||
if current_service.has_permission("inbound_sms")
|
||||
else ".delivery_status_callback"
|
||||
)
|
||||
return render_template(
|
||||
'views/api/index.html',
|
||||
"views/api/index.html",
|
||||
callbacks_link=callbacks_link,
|
||||
api_notifications=notification_api_client.get_api_notifications_for_service(service_id)
|
||||
api_notifications=notification_api_client.get_api_notifications_for_service(
|
||||
service_id
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/documentation")
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def api_documentation(service_id):
|
||||
return redirect(url_for('.documentation'), code=301)
|
||||
return redirect(url_for(".documentation"), code=301)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/whitelist", methods=['GET', 'POST'], endpoint='old_guest_list')
|
||||
@main.route("/services/<uuid:service_id>/api/guest-list", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/api/whitelist",
|
||||
methods=["GET", "POST"],
|
||||
endpoint="old_guest_list",
|
||||
)
|
||||
@main.route("/services/<uuid:service_id>/api/guest-list", methods=["GET", "POST"])
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def guest_list(service_id):
|
||||
form = GuestList()
|
||||
if form.validate_on_submit():
|
||||
service_api_client.update_guest_list(service_id, {
|
||||
'email_addresses': list(filter(None, form.email_addresses.data)),
|
||||
'phone_numbers': list(filter(None, form.phone_numbers.data))
|
||||
})
|
||||
flash('Guest list updated', 'default_with_tick')
|
||||
return redirect(url_for('.api_integration', service_id=service_id))
|
||||
service_api_client.update_guest_list(
|
||||
service_id,
|
||||
{
|
||||
"email_addresses": list(filter(None, form.email_addresses.data)),
|
||||
"phone_numbers": list(filter(None, form.phone_numbers.data)),
|
||||
},
|
||||
)
|
||||
flash("Guest list updated", "default_with_tick")
|
||||
return redirect(url_for(".api_integration", service_id=service_id))
|
||||
if not form.errors:
|
||||
form.populate(**service_api_client.get_guest_list(service_id))
|
||||
return render_template(
|
||||
'views/api/guest-list.html',
|
||||
form=form
|
||||
)
|
||||
return render_template("views/api/guest-list.html", form=form)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/keys")
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def api_keys(service_id):
|
||||
return render_template(
|
||||
'views/api/keys.html',
|
||||
"views/api/keys.html",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/keys/create", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_api_keys', restrict_admin_usage=True)
|
||||
@main.route("/services/<uuid:service_id>/api/keys/create", methods=["GET", "POST"])
|
||||
@user_has_permissions("manage_api_keys", restrict_admin_usage=True)
|
||||
def create_api_key(service_id):
|
||||
form = CreateKeyForm(current_service.api_keys)
|
||||
form.key_type.choices = [
|
||||
(KEY_TYPE_NORMAL, 'Live – sends to anyone'),
|
||||
(KEY_TYPE_TEAM, 'Team and guest list – limits who you can send to'),
|
||||
(KEY_TYPE_TEST, 'Test – pretends to send messages'),
|
||||
(KEY_TYPE_NORMAL, "Live – sends to anyone"),
|
||||
(KEY_TYPE_TEAM, "Team and guest list – limits who you can send to"),
|
||||
(KEY_TYPE_TEST, "Test – pretends to send messages"),
|
||||
]
|
||||
# preserve order of items extended by starting with empty dicts
|
||||
form.key_type.param_extensions = {'items': [{}, {}]}
|
||||
form.key_type.param_extensions = {"items": [{}, {}]}
|
||||
if current_service.trial_mode:
|
||||
form.key_type.param_extensions['items'][0] = {
|
||||
'disabled': True,
|
||||
'hint': {
|
||||
'html': Markup(
|
||||
'Not available because your service is in '
|
||||
'<a class="usa-link" href="/features/trial-mode">trial mode</a>')
|
||||
}
|
||||
form.key_type.param_extensions["items"][0] = {
|
||||
"disabled": True,
|
||||
"hint": {
|
||||
"html": Markup(
|
||||
"Not available because your service is in "
|
||||
'<a class="usa-link" href="/features/trial-mode">trial mode</a>'
|
||||
)
|
||||
},
|
||||
}
|
||||
if form.validate_on_submit():
|
||||
if current_service.trial_mode and form.key_type.data == KEY_TYPE_NORMAL:
|
||||
@@ -94,36 +102,38 @@ def create_api_key(service_id):
|
||||
secret = api_key_api_client.create_api_key(
|
||||
service_id=service_id,
|
||||
key_name=form.key_name.data,
|
||||
key_type=form.key_type.data
|
||||
key_type=form.key_type.data,
|
||||
)
|
||||
return render_template(
|
||||
'views/api/keys/show.html',
|
||||
"views/api/keys/show.html",
|
||||
secret=secret,
|
||||
service_id=service_id,
|
||||
key_name=email_safe(form.key_name.data, whitespace='_')
|
||||
key_name=email_safe(form.key_name.data, whitespace="_"),
|
||||
)
|
||||
return render_template(
|
||||
'views/api/keys/create.html',
|
||||
form=form
|
||||
)
|
||||
return render_template("views/api/keys/create.html", form=form)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/keys/revoke/<uuid:key_id>", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/api/keys/revoke/<uuid:key_id>", methods=["GET", "POST"]
|
||||
)
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def revoke_api_key(service_id, key_id):
|
||||
key_name = current_service.get_api_key(key_id)['name']
|
||||
if request.method == 'GET':
|
||||
flash([
|
||||
"Are you sure you want to revoke ‘{}’?".format(key_name),
|
||||
"You will not be able to use this API key to connect to U.S. Notify."
|
||||
], 'revoke this API key')
|
||||
return render_template(
|
||||
'views/api/keys.html',
|
||||
key_name = current_service.get_api_key(key_id)["name"]
|
||||
if request.method == "GET":
|
||||
flash(
|
||||
[
|
||||
"Are you sure you want to revoke ‘{}’?".format(key_name),
|
||||
"You will not be able to use this API key to connect to U.S. Notify.",
|
||||
],
|
||||
"revoke this API key",
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
return render_template(
|
||||
"views/api/keys.html",
|
||||
)
|
||||
elif request.method == "POST":
|
||||
api_key_api_client.revoke_api_key(service_id=service_id, key_id=key_id)
|
||||
flash('‘{}’ was revoked'.format(key_name), 'default_with_tick')
|
||||
return redirect(url_for('.api_keys', service_id=service_id))
|
||||
flash("‘{}’ was revoked".format(key_name), "default_with_tick")
|
||||
return redirect(url_for(".api_keys", service_id=service_id))
|
||||
|
||||
|
||||
def get_apis():
|
||||
@@ -131,13 +141,11 @@ def get_apis():
|
||||
inbound_api = None
|
||||
if current_service.service_callback_api:
|
||||
callback_api = service_api_client.get_service_callback_api(
|
||||
current_service.id,
|
||||
current_service.service_callback_api[0]
|
||||
current_service.id, current_service.service_callback_api[0]
|
||||
)
|
||||
if current_service.inbound_api:
|
||||
inbound_api = service_api_client.get_service_inbound_api(
|
||||
current_service.id,
|
||||
current_service.inbound_api[0]
|
||||
current_service.id, current_service.inbound_api[0]
|
||||
)
|
||||
|
||||
return (callback_api, inbound_api)
|
||||
@@ -147,71 +155,79 @@ def check_token_against_dummy_bearer(token):
|
||||
if token != dummy_bearer_token:
|
||||
return token
|
||||
else:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/callbacks", methods=['GET'])
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@main.route("/services/<uuid:service_id>/api/callbacks", methods=["GET"])
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def api_callbacks(service_id):
|
||||
if not current_service.has_permission('inbound_sms'):
|
||||
return redirect(url_for('.delivery_status_callback', service_id=service_id))
|
||||
if not current_service.has_permission("inbound_sms"):
|
||||
return redirect(url_for(".delivery_status_callback", service_id=service_id))
|
||||
|
||||
delivery_status_callback, received_text_messages_callback = get_apis()
|
||||
|
||||
return render_template(
|
||||
'views/api/callbacks.html',
|
||||
received_text_messages_callback=received_text_messages_callback['url']
|
||||
if received_text_messages_callback else None,
|
||||
delivery_status_callback=delivery_status_callback['url'] if delivery_status_callback else None
|
||||
"views/api/callbacks.html",
|
||||
received_text_messages_callback=received_text_messages_callback["url"]
|
||||
if received_text_messages_callback
|
||||
else None,
|
||||
delivery_status_callback=delivery_status_callback["url"]
|
||||
if delivery_status_callback
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
def get_delivery_status_callback_details():
|
||||
if current_service.service_callback_api:
|
||||
return service_api_client.get_service_callback_api(
|
||||
current_service.id,
|
||||
current_service.service_callback_api[0]
|
||||
current_service.id, current_service.service_callback_api[0]
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/callbacks/delivery-status-callback", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/api/callbacks/delivery-status-callback",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def delivery_status_callback(service_id):
|
||||
delivery_status_callback = get_delivery_status_callback_details()
|
||||
back_link = (
|
||||
'.api_callbacks' if current_service.has_permission('inbound_sms')
|
||||
else '.api_integration'
|
||||
".api_callbacks"
|
||||
if current_service.has_permission("inbound_sms")
|
||||
else ".api_integration"
|
||||
)
|
||||
|
||||
form = CallbackForm(
|
||||
url=delivery_status_callback.get('url') if delivery_status_callback else '',
|
||||
bearer_token=dummy_bearer_token if delivery_status_callback else ''
|
||||
url=delivery_status_callback.get("url") if delivery_status_callback else "",
|
||||
bearer_token=dummy_bearer_token if delivery_status_callback else "",
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if delivery_status_callback and form.url.data:
|
||||
if (
|
||||
delivery_status_callback.get('url') != form.url.data or
|
||||
form.bearer_token.data != dummy_bearer_token
|
||||
delivery_status_callback.get("url") != form.url.data
|
||||
or form.bearer_token.data != dummy_bearer_token
|
||||
):
|
||||
service_api_client.update_service_callback_api(
|
||||
service_id,
|
||||
url=form.url.data,
|
||||
bearer_token=check_token_against_dummy_bearer(form.bearer_token.data),
|
||||
bearer_token=check_token_against_dummy_bearer(
|
||||
form.bearer_token.data
|
||||
),
|
||||
user_id=current_user.id,
|
||||
callback_api_id=delivery_status_callback.get('id')
|
||||
callback_api_id=delivery_status_callback.get("id"),
|
||||
)
|
||||
elif delivery_status_callback and not form.url.data:
|
||||
service_api_client.delete_service_callback_api(
|
||||
service_id,
|
||||
delivery_status_callback['id'],
|
||||
delivery_status_callback["id"],
|
||||
)
|
||||
elif form.url.data:
|
||||
service_api_client.create_service_callback_api(
|
||||
service_id,
|
||||
url=form.url.data,
|
||||
bearer_token=form.bearer_token.data,
|
||||
user_id=current_user.id
|
||||
user_id=current_user.id,
|
||||
)
|
||||
else:
|
||||
# If no callback is set up and the user chooses to continue
|
||||
@@ -222,7 +238,7 @@ def delivery_status_callback(service_id):
|
||||
return redirect(url_for(back_link, service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
'views/api/callbacks/delivery-status-callback.html',
|
||||
"views/api/callbacks/delivery-status-callback.html",
|
||||
back_link=back_link,
|
||||
form=form,
|
||||
)
|
||||
@@ -231,50 +247,56 @@ def delivery_status_callback(service_id):
|
||||
def get_received_text_messages_callback():
|
||||
if current_service.inbound_api:
|
||||
return service_api_client.get_service_inbound_api(
|
||||
current_service.id,
|
||||
current_service.inbound_api[0]
|
||||
current_service.id, current_service.inbound_api[0]
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/api/callbacks/received-text-messages-callback", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_api_keys')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/api/callbacks/received-text-messages-callback",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_api_keys")
|
||||
def received_text_messages_callback(service_id):
|
||||
if not current_service.has_permission('inbound_sms'):
|
||||
return redirect(url_for('.api_integration', service_id=service_id))
|
||||
if not current_service.has_permission("inbound_sms"):
|
||||
return redirect(url_for(".api_integration", service_id=service_id))
|
||||
|
||||
received_text_messages_callback = get_received_text_messages_callback()
|
||||
form = CallbackForm(
|
||||
url=received_text_messages_callback.get('url') if received_text_messages_callback else '',
|
||||
bearer_token=dummy_bearer_token if received_text_messages_callback else ''
|
||||
url=received_text_messages_callback.get("url")
|
||||
if received_text_messages_callback
|
||||
else "",
|
||||
bearer_token=dummy_bearer_token if received_text_messages_callback else "",
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if received_text_messages_callback and form.url.data:
|
||||
if (
|
||||
received_text_messages_callback.get('url') != form.url.data or
|
||||
form.bearer_token.data != dummy_bearer_token
|
||||
received_text_messages_callback.get("url") != form.url.data
|
||||
or form.bearer_token.data != dummy_bearer_token
|
||||
):
|
||||
service_api_client.update_service_inbound_api(
|
||||
service_id,
|
||||
url=form.url.data,
|
||||
bearer_token=check_token_against_dummy_bearer(form.bearer_token.data),
|
||||
bearer_token=check_token_against_dummy_bearer(
|
||||
form.bearer_token.data
|
||||
),
|
||||
user_id=current_user.id,
|
||||
inbound_api_id=received_text_messages_callback.get('id')
|
||||
inbound_api_id=received_text_messages_callback.get("id"),
|
||||
)
|
||||
elif received_text_messages_callback and not form.url.data:
|
||||
service_api_client.delete_service_inbound_api(
|
||||
service_id,
|
||||
received_text_messages_callback['id'],
|
||||
received_text_messages_callback["id"],
|
||||
)
|
||||
elif form.url.data:
|
||||
service_api_client.create_service_inbound_api(
|
||||
service_id,
|
||||
url=form.url.data,
|
||||
bearer_token=form.bearer_token.data,
|
||||
user_id=current_user.id
|
||||
user_id=current_user.id,
|
||||
)
|
||||
return redirect(url_for('.api_callbacks', service_id=service_id))
|
||||
return redirect(url_for(".api_callbacks", service_id=service_id))
|
||||
return render_template(
|
||||
'views/api/callbacks/received-text-messages-callback.html',
|
||||
"views/api/callbacks/received-text-messages-callback.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
@@ -10,12 +10,12 @@ from app.utils.user import user_is_logged_in
|
||||
|
||||
@main.route("/services")
|
||||
def choose_service():
|
||||
raise PermanentRedirect(url_for('.choose_account'))
|
||||
raise PermanentRedirect(url_for(".choose_account"))
|
||||
|
||||
|
||||
@main.route("/services-or-dashboard")
|
||||
def services_or_dashboard():
|
||||
raise PermanentRedirect(url_for('.show_accounts_or_dashboard'))
|
||||
raise PermanentRedirect(url_for(".show_accounts_or_dashboard"))
|
||||
|
||||
|
||||
@main.route("/accounts")
|
||||
@@ -25,10 +25,12 @@ def choose_account():
|
||||
if current_user.platform_admin:
|
||||
org_count, live_service_count = (
|
||||
len(AllOrganizations()),
|
||||
status_api_client.get_count_of_live_services_and_organizations()['services'],
|
||||
status_api_client.get_count_of_live_services_and_organizations()[
|
||||
"services"
|
||||
],
|
||||
)
|
||||
return render_template(
|
||||
'views/choose-account.html',
|
||||
"views/choose-account.html",
|
||||
can_add_service=current_user.is_gov_user,
|
||||
org_count=org_count,
|
||||
live_service_count=live_service_count,
|
||||
@@ -37,22 +39,30 @@ def choose_account():
|
||||
|
||||
@main.route("/accounts-or-dashboard")
|
||||
def show_accounts_or_dashboard():
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for('.index'))
|
||||
return redirect(url_for(".index"))
|
||||
|
||||
service_id = session.get('service_id')
|
||||
if service_id and (current_user.belongs_to_service(service_id) or current_user.platform_admin):
|
||||
return redirect(url_for('.service_dashboard', service_id=service_id))
|
||||
service_id = session.get("service_id")
|
||||
if service_id and (
|
||||
current_user.belongs_to_service(service_id) or current_user.platform_admin
|
||||
):
|
||||
return redirect(url_for(".service_dashboard", service_id=service_id))
|
||||
|
||||
organization_id = session.get('organization_id')
|
||||
if organization_id and (current_user.belongs_to_organization(organization_id) or current_user.platform_admin):
|
||||
return redirect(url_for('.organization_dashboard', org_id=organization_id))
|
||||
organization_id = session.get("organization_id")
|
||||
if organization_id and (
|
||||
current_user.belongs_to_organization(organization_id)
|
||||
or current_user.platform_admin
|
||||
):
|
||||
return redirect(url_for(".organization_dashboard", org_id=organization_id))
|
||||
|
||||
if len(current_user.service_ids) == 1 and not current_user.organization_ids:
|
||||
return redirect(url_for('.service_dashboard', service_id=current_user.service_ids[0]))
|
||||
return redirect(
|
||||
url_for(".service_dashboard", service_id=current_user.service_ids[0])
|
||||
)
|
||||
|
||||
if len(current_user.organization_ids) == 1 and not current_user.trial_mode_services:
|
||||
return redirect(url_for('.organization_dashboard', org_id=current_user.organization_ids[0]))
|
||||
return redirect(
|
||||
url_for(".organization_dashboard", org_id=current_user.organization_ids[0])
|
||||
)
|
||||
|
||||
return redirect(url_for('.choose_account'))
|
||||
return redirect(url_for(".choose_account"))
|
||||
|
||||
@@ -7,55 +7,59 @@ from app.models.user import User
|
||||
from app.utils.login import redirect_to_sign_in
|
||||
|
||||
|
||||
@main.route('/resend-email-verification')
|
||||
@main.route("/resend-email-verification")
|
||||
@redirect_to_sign_in
|
||||
def resend_email_verification():
|
||||
user = User.from_email_address(session['user_details']['email'])
|
||||
user = User.from_email_address(session["user_details"]["email"])
|
||||
user.send_verify_email()
|
||||
return render_template('views/resend-email-verification.html', email=user.email_address)
|
||||
return render_template(
|
||||
"views/resend-email-verification.html", email=user.email_address
|
||||
)
|
||||
|
||||
|
||||
@main.route('/text-not-received', methods=['GET', 'POST'])
|
||||
@main.route("/text-not-received", methods=["GET", "POST"])
|
||||
@redirect_to_sign_in
|
||||
def check_and_resend_text_code():
|
||||
user = User.from_email_address(session['user_details']['email'])
|
||||
redirect_url = request.args.get('next')
|
||||
user = User.from_email_address(session["user_details"]["email"])
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if user.state == 'active':
|
||||
if user.state == "active":
|
||||
# this is a verified user and therefore redirect to page to request resend without edit mobile
|
||||
return render_template('views/verification-not-received.html', redirect_url=redirect_url)
|
||||
return render_template(
|
||||
"views/verification-not-received.html", redirect_url=redirect_url
|
||||
)
|
||||
|
||||
form = TextNotReceivedForm(mobile_number=user.mobile_number)
|
||||
if form.validate_on_submit():
|
||||
user.send_verify_code(to=form.mobile_number.data)
|
||||
user.update(mobile_number=form.mobile_number.data)
|
||||
return redirect(url_for('.verify', next=redirect_url))
|
||||
return redirect(url_for(".verify", next=redirect_url))
|
||||
|
||||
return render_template('views/text-not-received.html', form=form)
|
||||
return render_template("views/text-not-received.html", form=form)
|
||||
|
||||
|
||||
@main.route('/send-new-code', methods=['GET'])
|
||||
@main.route("/send-new-code", methods=["GET"])
|
||||
@redirect_to_sign_in
|
||||
def check_and_resend_verification_code():
|
||||
user = User.from_email_address(session['user_details']['email'])
|
||||
user = User.from_email_address(session["user_details"]["email"])
|
||||
user.send_verify_code()
|
||||
redirect_url = request.args.get('next')
|
||||
if user.state == 'pending':
|
||||
return redirect(url_for('main.verify', next=redirect_url))
|
||||
redirect_url = request.args.get("next")
|
||||
if user.state == "pending":
|
||||
return redirect(url_for("main.verify", next=redirect_url))
|
||||
else:
|
||||
return redirect(url_for('main.two_factor_sms', next=redirect_url))
|
||||
return redirect(url_for("main.two_factor_sms", next=redirect_url))
|
||||
|
||||
|
||||
@main.route('/email-not-received', methods=['GET'])
|
||||
@main.route("/email-not-received", methods=["GET"])
|
||||
@redirect_to_sign_in
|
||||
def email_not_received():
|
||||
redirect_url = request.args.get('next')
|
||||
return render_template('views/email-not-received.html', redirect_url=redirect_url)
|
||||
redirect_url = request.args.get("next")
|
||||
return render_template("views/email-not-received.html", redirect_url=redirect_url)
|
||||
|
||||
|
||||
@main.route('/send-new-email-token', methods=['GET'])
|
||||
@main.route("/send-new-email-token", methods=["GET"])
|
||||
@redirect_to_sign_in
|
||||
def resend_email_link():
|
||||
user_api_client.send_verify_code(session['user_details']['id'], 'email', None)
|
||||
session.pop('user_details')
|
||||
return redirect(url_for('main.two_factor_email_sent', email_resent=True))
|
||||
user_api_client.send_verify_code(session["user_details"]["id"], "email", None)
|
||||
session.pop("user_details")
|
||||
return redirect(url_for("main.two_factor_email_sent", email_resent=True))
|
||||
|
||||
@@ -12,77 +12,86 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def conversation(service_id, notification_id):
|
||||
|
||||
user_number = get_user_number(service_id, notification_id)
|
||||
|
||||
return render_template(
|
||||
'views/conversations/conversation.html',
|
||||
"views/conversations/conversation.html",
|
||||
user_number=user_number,
|
||||
partials=get_conversation_partials(service_id, user_number),
|
||||
updates_url=url_for('.conversation_updates', service_id=service_id, notification_id=notification_id),
|
||||
updates_url=url_for(
|
||||
".conversation_updates",
|
||||
service_id=service_id,
|
||||
notification_id=notification_id,
|
||||
),
|
||||
notification_id=notification_id,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>.json")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def conversation_updates(service_id, notification_id):
|
||||
|
||||
return jsonify(get_conversation_partials(
|
||||
service_id,
|
||||
get_user_number(service_id, notification_id)
|
||||
))
|
||||
return jsonify(
|
||||
get_conversation_partials(
|
||||
service_id, get_user_number(service_id, notification_id)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with")
|
||||
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/from-folder/<uuid:from_folder>")
|
||||
@user_has_permissions('send_messages')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with"
|
||||
)
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/from-folder/<uuid:from_folder>"
|
||||
)
|
||||
@user_has_permissions("send_messages")
|
||||
def conversation_reply(
|
||||
service_id,
|
||||
notification_id,
|
||||
from_folder=None,
|
||||
):
|
||||
return render_template(
|
||||
'views/templates/choose-reply.html',
|
||||
"views/templates/choose-reply.html",
|
||||
templates_and_folders=TemplateList(
|
||||
current_service,
|
||||
template_folder_id=from_folder,
|
||||
user=current_user,
|
||||
template_type='sms'
|
||||
template_type="sms",
|
||||
),
|
||||
template_folder_path=current_service.get_template_folder_path(from_folder),
|
||||
search_form=SearchByNameForm(),
|
||||
notification_id=notification_id,
|
||||
template_type='sms'
|
||||
template_type="sms",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/<uuid:template_id>")
|
||||
@user_has_permissions('send_messages')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/<uuid:template_id>"
|
||||
)
|
||||
@user_has_permissions("send_messages")
|
||||
def conversation_reply_with_template(
|
||||
service_id,
|
||||
notification_id,
|
||||
template_id,
|
||||
):
|
||||
session["recipient"] = get_user_number(service_id, notification_id)
|
||||
session["placeholders"] = {"phone number": session["recipient"]}
|
||||
|
||||
session['recipient'] = get_user_number(service_id, notification_id)
|
||||
session['placeholders'] = {'phone number': session['recipient']}
|
||||
|
||||
return redirect(url_for(
|
||||
'main.send_one_off_step',
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=1,
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.send_one_off_step",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=1,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_conversation_partials(service_id, user_number):
|
||||
|
||||
return {
|
||||
'messages': render_template(
|
||||
'views/conversations/messages.html',
|
||||
"messages": render_template(
|
||||
"views/conversations/messages.html",
|
||||
conversation=get_sms_thread(service_id, user_number),
|
||||
)
|
||||
}
|
||||
@@ -90,44 +99,54 @@ def get_conversation_partials(service_id, user_number):
|
||||
|
||||
def get_user_number(service_id, notification_id):
|
||||
try:
|
||||
user_number = service_api_client.get_inbound_sms_by_id(service_id, notification_id)['user_number']
|
||||
user_number = service_api_client.get_inbound_sms_by_id(
|
||||
service_id, notification_id
|
||||
)["user_number"]
|
||||
except HTTPError as e:
|
||||
if e.status_code != 404:
|
||||
raise
|
||||
user_number = notification_api_client.get_notification(service_id, notification_id)['to']
|
||||
user_number = notification_api_client.get_notification(
|
||||
service_id, notification_id
|
||||
)["to"]
|
||||
return format_phone_number_human_readable(user_number)
|
||||
|
||||
|
||||
def get_sms_thread(service_id, user_number):
|
||||
|
||||
for notification in sorted(( # noqa: B020
|
||||
notification_api_client.get_notifications_for_service(service_id,
|
||||
to=user_number,
|
||||
template_type='sms')['notifications'] +
|
||||
service_api_client.get_inbound_sms(service_id, user_number=user_number)['data']
|
||||
), key=lambda notification: notification['created_at']):
|
||||
|
||||
is_inbound = ('notify_number' in notification)
|
||||
redact_personalisation = not is_inbound and notification['template']['redact_personalisation']
|
||||
for notification in sorted(
|
||||
( # noqa: B020
|
||||
notification_api_client.get_notifications_for_service(
|
||||
service_id, to=user_number, template_type="sms"
|
||||
)["notifications"]
|
||||
+ service_api_client.get_inbound_sms(service_id, user_number=user_number)[
|
||||
"data"
|
||||
]
|
||||
),
|
||||
key=lambda notification: notification["created_at"],
|
||||
):
|
||||
is_inbound = "notify_number" in notification
|
||||
redact_personalisation = (
|
||||
not is_inbound and notification["template"]["redact_personalisation"]
|
||||
)
|
||||
|
||||
if redact_personalisation:
|
||||
notification['personalisation'] = {}
|
||||
notification["personalisation"] = {}
|
||||
|
||||
yield {
|
||||
'inbound': is_inbound,
|
||||
'content': SMSPreviewTemplate(
|
||||
"inbound": is_inbound,
|
||||
"content": SMSPreviewTemplate(
|
||||
{
|
||||
'template_type': 'sms',
|
||||
'content': (
|
||||
notification['content'] if is_inbound else
|
||||
notification['template']['content']
|
||||
)
|
||||
"template_type": "sms",
|
||||
"content": (
|
||||
notification["content"]
|
||||
if is_inbound
|
||||
else notification["template"]["content"]
|
||||
),
|
||||
},
|
||||
notification.get('personalisation'),
|
||||
notification.get("personalisation"),
|
||||
downgrade_non_sms_characters=(not is_inbound),
|
||||
redact_missing_personalisation=redact_personalisation,
|
||||
),
|
||||
'created_at': notification['created_at'],
|
||||
'status': notification.get('status'),
|
||||
'id': notification['id'],
|
||||
"created_at": notification["created_at"],
|
||||
"status": notification.get("status"),
|
||||
"id": notification["id"],
|
||||
}
|
||||
|
||||
@@ -30,136 +30,145 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard")
|
||||
@user_has_permissions('view_activity', 'send_messages')
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
def old_service_dashboard(service_id):
|
||||
return redirect(url_for('.service_dashboard', service_id=service_id))
|
||||
return redirect(url_for(".service_dashboard", service_id=service_id))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>")
|
||||
@user_has_permissions()
|
||||
def service_dashboard(service_id):
|
||||
if session.get("invited_user_id"):
|
||||
session.pop("invited_user_id", None)
|
||||
session["service_id"] = service_id
|
||||
|
||||
if session.get('invited_user_id'):
|
||||
session.pop('invited_user_id', None)
|
||||
session['service_id'] = service_id
|
||||
|
||||
if not current_user.has_permissions('view_activity'):
|
||||
return redirect(url_for('main.choose_template', service_id=service_id))
|
||||
if not current_user.has_permissions("view_activity"):
|
||||
return redirect(url_for("main.choose_template", service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
'views/dashboard/dashboard.html',
|
||||
"views/dashboard/dashboard.html",
|
||||
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||
partials=get_dashboard_partials(service_id)
|
||||
partials=get_dashboard_partials(service_id),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard.json")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def service_dashboard_updates(service_id):
|
||||
return jsonify(**get_dashboard_partials(service_id))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/template-activity")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def template_history(service_id):
|
||||
|
||||
return redirect(url_for('main.template_usage', service_id=service_id), code=301)
|
||||
return redirect(url_for("main.template_usage", service_id=service_id), code=301)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/template-usage")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def template_usage(service_id):
|
||||
|
||||
year, current_financial_year = requested_and_current_financial_year(request)
|
||||
stats = template_statistics_client.get_monthly_template_usage_for_service(service_id, year)
|
||||
stats = template_statistics_client.get_monthly_template_usage_for_service(
|
||||
service_id, year
|
||||
)
|
||||
|
||||
stats = sorted(stats, key=lambda x: (x['count']), reverse=True)
|
||||
stats = sorted(stats, key=lambda x: (x["count"]), reverse=True)
|
||||
|
||||
def get_monthly_template_stats(month_name, stats):
|
||||
return {
|
||||
'name': month_name,
|
||||
'templates_used': [
|
||||
"name": month_name,
|
||||
"templates_used": [
|
||||
{
|
||||
'id': stat['template_id'],
|
||||
'name': stat['name'],
|
||||
'type': stat['type'],
|
||||
'requested_count': stat['count']
|
||||
"id": stat["template_id"],
|
||||
"name": stat["name"],
|
||||
"type": stat["type"],
|
||||
"requested_count": stat["count"],
|
||||
}
|
||||
for stat in stats
|
||||
if calendar.month_name[int(stat['month'])] == month_name
|
||||
if calendar.month_name[int(stat["month"])] == month_name
|
||||
],
|
||||
}
|
||||
|
||||
months = [
|
||||
get_monthly_template_stats(month, stats)
|
||||
for month in get_months_for_financial_year(year, time_format='%B')
|
||||
for month in get_months_for_financial_year(year, time_format="%B")
|
||||
]
|
||||
|
||||
return render_template(
|
||||
'views/dashboard/all-template-statistics.html',
|
||||
"views/dashboard/all-template-statistics.html",
|
||||
months=months,
|
||||
stats=stats,
|
||||
most_used_template_count=max(
|
||||
max((
|
||||
template['requested_count']
|
||||
for template in month['templates_used']
|
||||
), default=0)
|
||||
max(
|
||||
(template["requested_count"] for template in month["templates_used"]),
|
||||
default=0,
|
||||
)
|
||||
for month in months
|
||||
),
|
||||
years=get_tuples_of_financial_years(
|
||||
partial(url_for, '.template_usage', service_id=service_id),
|
||||
partial(url_for, ".template_usage", service_id=service_id),
|
||||
start=current_financial_year - 2,
|
||||
end=current_financial_year,
|
||||
),
|
||||
selected_year=year
|
||||
selected_year=year,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/usage")
|
||||
@user_has_permissions('manage_service', allow_org_user=True)
|
||||
@user_has_permissions("manage_service", allow_org_user=True)
|
||||
def usage(service_id):
|
||||
year, current_financial_year = requested_and_current_financial_year(request)
|
||||
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(service_id, year)
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
service_id, year
|
||||
)
|
||||
units = billing_api_client.get_monthly_usage_for_service(service_id, year)
|
||||
yearly_usage = billing_api_client.get_annual_usage_for_service(service_id, year)
|
||||
|
||||
more_stats = format_monthly_stats_to_list(
|
||||
service_api_client.get_monthly_notification_stats(service_id, year)['data']
|
||||
service_api_client.get_monthly_notification_stats(service_id, year)["data"]
|
||||
)
|
||||
if year == current_financial_year:
|
||||
# This includes Oct, Nov, Dec
|
||||
# but we don't need next year's data yet
|
||||
more_stats = [month for month in more_stats if month['name'] in ['October', 'November', 'December']]
|
||||
more_stats = [
|
||||
month
|
||||
for month in more_stats
|
||||
if month["name"] in ["October", "November", "December"]
|
||||
]
|
||||
elif year == (current_financial_year + 1):
|
||||
# This is all the other months
|
||||
# and we need last year's data
|
||||
more_stats = [month for month in more_stats if month['name'] not in ['October', 'November', 'December']]
|
||||
more_stats = [
|
||||
month
|
||||
for month in more_stats
|
||||
if month["name"] not in ["October", "November", "December"]
|
||||
]
|
||||
|
||||
return render_template(
|
||||
'views/usage.html',
|
||||
"views/usage.html",
|
||||
months=list(get_monthly_usage_breakdown(year, units, more_stats)),
|
||||
selected_year=year,
|
||||
years=get_tuples_of_financial_years(
|
||||
partial(url_for, '.usage', service_id=service_id),
|
||||
partial(url_for, ".usage", service_id=service_id),
|
||||
start=current_financial_year - 2,
|
||||
end=current_financial_year,
|
||||
),
|
||||
**get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
|
||||
**get_annual_usage_breakdown(yearly_usage, free_sms_allowance),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/monthly")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def monthly(service_id):
|
||||
year, current_financial_year = requested_and_current_financial_year(request)
|
||||
return render_template(
|
||||
'views/dashboard/monthly.html',
|
||||
"views/dashboard/monthly.html",
|
||||
months=format_monthly_stats_to_list(
|
||||
service_api_client.get_monthly_notification_stats(service_id, year)['data']
|
||||
service_api_client.get_monthly_notification_stats(service_id, year)["data"]
|
||||
),
|
||||
years=get_tuples_of_financial_years(
|
||||
partial_url=partial(url_for, '.monthly', service_id=service_id),
|
||||
partial_url=partial(url_for, ".monthly", service_id=service_id),
|
||||
start=current_financial_year - 2,
|
||||
end=current_financial_year,
|
||||
),
|
||||
@@ -168,53 +177,61 @@ def monthly(service_id):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/inbox")
|
||||
@user_has_permissions('view_activity')
|
||||
@service_has_permission('inbound_sms')
|
||||
@user_has_permissions("view_activity")
|
||||
@service_has_permission("inbound_sms")
|
||||
def inbox(service_id):
|
||||
|
||||
return render_template(
|
||||
'views/dashboard/inbox.html',
|
||||
"views/dashboard/inbox.html",
|
||||
partials=get_inbox_partials(service_id),
|
||||
updates_url=url_for('.inbox_updates', service_id=service_id, page=request.args.get('page')),
|
||||
updates_url=url_for(
|
||||
".inbox_updates", service_id=service_id, page=request.args.get("page")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/inbox.json")
|
||||
@user_has_permissions('view_activity')
|
||||
@service_has_permission('inbound_sms')
|
||||
@user_has_permissions("view_activity")
|
||||
@service_has_permission("inbound_sms")
|
||||
def inbox_updates(service_id):
|
||||
|
||||
return jsonify(get_inbox_partials(service_id))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/inbox.csv")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def inbox_download(service_id):
|
||||
return Response(
|
||||
Spreadsheet.from_rows(
|
||||
[[
|
||||
'Phone number',
|
||||
'Message',
|
||||
'Received',
|
||||
]] + [[
|
||||
format_phone_number_human_readable(message['user_number']),
|
||||
message['content'].lstrip(('=+-@')),
|
||||
format_datetime_numeric(message['created_at']),
|
||||
] for message in service_api_client.get_inbound_sms(service_id)['data']]
|
||||
[
|
||||
[
|
||||
"Phone number",
|
||||
"Message",
|
||||
"Received",
|
||||
]
|
||||
]
|
||||
+ [
|
||||
[
|
||||
format_phone_number_human_readable(message["user_number"]),
|
||||
message["content"].lstrip(("=+-@")),
|
||||
format_datetime_numeric(message["created_at"]),
|
||||
]
|
||||
for message in service_api_client.get_inbound_sms(service_id)["data"]
|
||||
]
|
||||
).as_csv_data,
|
||||
mimetype='text/csv',
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
'Content-Disposition': 'inline; filename="Received text messages {}.csv"'.format(
|
||||
"Content-Disposition": 'inline; filename="Received text messages {}.csv"'.format(
|
||||
format_date_numeric(datetime.utcnow().isoformat())
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_inbox_partials(service_id):
|
||||
page = int(request.args.get('page', 1))
|
||||
inbound_messages_data = service_api_client.get_most_recent_inbound_sms(service_id, page=page)
|
||||
inbound_messages = inbound_messages_data['data']
|
||||
page = int(request.args.get("page", 1))
|
||||
inbound_messages_data = service_api_client.get_most_recent_inbound_sms(
|
||||
service_id, page=page
|
||||
)
|
||||
inbound_messages = inbound_messages_data["data"]
|
||||
if not inbound_messages:
|
||||
inbound_number = current_service.inbound_number
|
||||
else:
|
||||
@@ -222,36 +239,43 @@ def get_inbox_partials(service_id):
|
||||
|
||||
prev_page = None
|
||||
if page > 1:
|
||||
prev_page = generate_previous_dict('main.inbox', service_id, page)
|
||||
prev_page = generate_previous_dict("main.inbox", service_id, page)
|
||||
next_page = None
|
||||
if inbound_messages_data['has_next']:
|
||||
next_page = generate_next_dict('main.inbox', service_id, page)
|
||||
if inbound_messages_data["has_next"]:
|
||||
next_page = generate_next_dict("main.inbox", service_id, page)
|
||||
|
||||
return {'messages': render_template(
|
||||
'views/dashboard/_inbox_messages.html',
|
||||
messages=inbound_messages,
|
||||
inbound_number=inbound_number,
|
||||
prev_page=prev_page,
|
||||
next_page=next_page
|
||||
)}
|
||||
return {
|
||||
"messages": render_template(
|
||||
"views/dashboard/_inbox_messages.html",
|
||||
messages=inbound_messages,
|
||||
inbound_number=inbound_number,
|
||||
prev_page=prev_page,
|
||||
next_page=next_page,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def filter_out_cancelled_stats(template_statistics):
|
||||
return [s for s in template_statistics if s["status"] != "cancelled"]
|
||||
|
||||
|
||||
def aggregate_template_usage(template_statistics, sort_key='count'):
|
||||
def aggregate_template_usage(template_statistics, sort_key="count"):
|
||||
template_statistics = filter_out_cancelled_stats(template_statistics)
|
||||
templates = []
|
||||
for k, v in groupby(sorted(template_statistics, key=lambda x: x['template_id']), key=lambda x: x['template_id']):
|
||||
for k, v in groupby(
|
||||
sorted(template_statistics, key=lambda x: x["template_id"]),
|
||||
key=lambda x: x["template_id"],
|
||||
):
|
||||
template_stats = list(v)
|
||||
|
||||
templates.append({
|
||||
"template_id": k,
|
||||
"template_name": template_stats[0]['template_name'],
|
||||
"template_type": template_stats[0]['template_type'],
|
||||
"count": sum(s['count'] for s in template_stats)
|
||||
})
|
||||
templates.append(
|
||||
{
|
||||
"template_id": k,
|
||||
"template_name": template_stats[0]["template_name"],
|
||||
"template_type": template_stats[0]["template_type"],
|
||||
"count": sum(s["count"] for s in template_stats),
|
||||
}
|
||||
)
|
||||
|
||||
return sorted(templates, key=lambda x: x[sort_key], reverse=True)
|
||||
|
||||
@@ -259,9 +283,8 @@ def aggregate_template_usage(template_statistics, sort_key='count'):
|
||||
def aggregate_notifications_stats(template_statistics):
|
||||
template_statistics = filter_out_cancelled_stats(template_statistics)
|
||||
notifications = {
|
||||
template_type: {
|
||||
status: 0 for status in ('requested', 'delivered', 'failed')
|
||||
} for template_type in ["sms", "email"]
|
||||
template_type: {status: 0 for status in ("requested", "delivered", "failed")}
|
||||
for template_type in ["sms", "email"]
|
||||
}
|
||||
for stat in template_statistics:
|
||||
notifications[stat["template_type"]]["requested"] += stat["count"]
|
||||
@@ -274,11 +297,13 @@ def aggregate_notifications_stats(template_statistics):
|
||||
|
||||
|
||||
def get_dashboard_partials(service_id):
|
||||
all_statistics = template_statistics_client.get_template_statistics_for_service(service_id, limit_days=7)
|
||||
all_statistics = template_statistics_client.get_template_statistics_for_service(
|
||||
service_id, limit_days=7
|
||||
)
|
||||
template_statistics = aggregate_template_usage(all_statistics)
|
||||
stats = aggregate_notifications_stats(all_statistics)
|
||||
|
||||
dashboard_totals = get_dashboard_totals(stats),
|
||||
dashboard_totals = (get_dashboard_totals(stats),)
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
current_service.id,
|
||||
get_current_financial_year(),
|
||||
@@ -288,26 +313,26 @@ def get_dashboard_partials(service_id):
|
||||
get_current_financial_year(),
|
||||
)
|
||||
return {
|
||||
'upcoming': render_template(
|
||||
'views/dashboard/_upcoming.html',
|
||||
"upcoming": render_template(
|
||||
"views/dashboard/_upcoming.html",
|
||||
),
|
||||
'inbox': render_template(
|
||||
'views/dashboard/_inbox.html',
|
||||
"inbox": render_template(
|
||||
"views/dashboard/_inbox.html",
|
||||
),
|
||||
'totals': render_template(
|
||||
'views/dashboard/_totals.html',
|
||||
"totals": render_template(
|
||||
"views/dashboard/_totals.html",
|
||||
service_id=service_id,
|
||||
statistics=dashboard_totals[0],
|
||||
),
|
||||
'template-statistics': render_template(
|
||||
'views/dashboard/template-statistics.html',
|
||||
"template-statistics": render_template(
|
||||
"views/dashboard/template-statistics.html",
|
||||
template_statistics=template_statistics,
|
||||
most_used_template_count=max(
|
||||
[row['count'] for row in template_statistics] or [0]
|
||||
[row["count"] for row in template_statistics] or [0]
|
||||
),
|
||||
),
|
||||
'usage': render_template(
|
||||
'views/dashboard/_usage.html',
|
||||
"usage": render_template(
|
||||
"views/dashboard/_usage.html",
|
||||
**get_annual_usage_breakdown(yearly_usage, free_sms_allowance),
|
||||
),
|
||||
}
|
||||
@@ -315,39 +340,45 @@ def get_dashboard_partials(service_id):
|
||||
|
||||
def get_dashboard_totals(statistics):
|
||||
for msg_type in statistics.values():
|
||||
msg_type['failed_percentage'] = get_formatted_percentage(msg_type['failed'], msg_type['requested'])
|
||||
msg_type['show_warning'] = float(msg_type['failed_percentage']) > 3
|
||||
msg_type["failed_percentage"] = get_formatted_percentage(
|
||||
msg_type["failed"], msg_type["requested"]
|
||||
)
|
||||
msg_type["show_warning"] = float(msg_type["failed_percentage"]) > 3
|
||||
return statistics
|
||||
|
||||
|
||||
def get_annual_usage_breakdown(usage, free_sms_fragment_limit):
|
||||
sms = get_usage_breakdown_by_type(usage, 'sms')
|
||||
sms_chargeable_units = sum(row['chargeable_units'] for row in sms)
|
||||
sms = get_usage_breakdown_by_type(usage, "sms")
|
||||
sms_chargeable_units = sum(row["chargeable_units"] for row in sms)
|
||||
sms_free_allowance = free_sms_fragment_limit
|
||||
sms_cost = sum(row['cost'] for row in sms)
|
||||
sms_cost = sum(row["cost"] for row in sms)
|
||||
|
||||
emails = get_usage_breakdown_by_type(usage, 'email')
|
||||
emails_sent = sum(row['notifications_sent'] for row in emails)
|
||||
emails = get_usage_breakdown_by_type(usage, "email")
|
||||
emails_sent = sum(row["notifications_sent"] for row in emails)
|
||||
|
||||
return {
|
||||
'emails_sent': emails_sent,
|
||||
'sms_free_allowance': sms_free_allowance,
|
||||
'sms_sent': sms_chargeable_units,
|
||||
'sms_allowance_remaining': max(0, (sms_free_allowance - sms_chargeable_units)),
|
||||
'sms_cost': sms_cost,
|
||||
'sms_breakdown': sms,
|
||||
"emails_sent": emails_sent,
|
||||
"sms_free_allowance": sms_free_allowance,
|
||||
"sms_sent": sms_chargeable_units,
|
||||
"sms_allowance_remaining": max(0, (sms_free_allowance - sms_chargeable_units)),
|
||||
"sms_cost": sms_cost,
|
||||
"sms_breakdown": sms,
|
||||
}
|
||||
|
||||
|
||||
def format_monthly_stats_to_list(historical_stats):
|
||||
return sorted((
|
||||
dict(
|
||||
date=key,
|
||||
future=yyyy_mm_to_datetime(key) > datetime.utcnow(),
|
||||
name=yyyy_mm_to_datetime(key).strftime('%B'),
|
||||
**aggregate_status_types(value)
|
||||
) for key, value in historical_stats.items()
|
||||
), key=lambda x: x['date'])
|
||||
return sorted(
|
||||
(
|
||||
dict(
|
||||
date=key,
|
||||
future=yyyy_mm_to_datetime(key) > datetime.utcnow(),
|
||||
name=yyyy_mm_to_datetime(key).strftime("%B"),
|
||||
**aggregate_status_types(value),
|
||||
)
|
||||
for key, value in historical_stats.items()
|
||||
),
|
||||
key=lambda x: x["date"],
|
||||
)
|
||||
|
||||
|
||||
def yyyy_mm_to_datetime(string):
|
||||
@@ -355,24 +386,22 @@ def yyyy_mm_to_datetime(string):
|
||||
|
||||
|
||||
def aggregate_status_types(counts_dict):
|
||||
return get_dashboard_totals({
|
||||
'{}_counts'.format(message_type): {
|
||||
'failed': sum(
|
||||
stats.get(status, 0) for status in FAILURE_STATUSES
|
||||
),
|
||||
'requested': sum(
|
||||
stats.get(status, 0) for status in REQUESTED_STATUSES
|
||||
)
|
||||
} for message_type, stats in counts_dict.items()
|
||||
})
|
||||
return get_dashboard_totals(
|
||||
{
|
||||
"{}_counts".format(message_type): {
|
||||
"failed": sum(stats.get(status, 0) for status in FAILURE_STATUSES),
|
||||
"requested": sum(stats.get(status, 0) for status in REQUESTED_STATUSES),
|
||||
}
|
||||
for message_type, stats in counts_dict.items()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_months_for_financial_year(year, time_format='%B'):
|
||||
def get_months_for_financial_year(year, time_format="%B"):
|
||||
return [
|
||||
month.strftime(time_format)
|
||||
for month in (
|
||||
get_months_for_year(10, 13, year) +
|
||||
get_months_for_year(1, 10, year + 1)
|
||||
get_months_for_year(10, 13, year) + get_months_for_year(1, 10, year + 1)
|
||||
)
|
||||
if month < datetime.now()
|
||||
]
|
||||
@@ -383,32 +412,36 @@ def get_months_for_year(start, end, year):
|
||||
|
||||
|
||||
def get_usage_breakdown_by_type(usage, notification_type):
|
||||
return [row for row in usage if row['notification_type'] == notification_type]
|
||||
return [row for row in usage if row["notification_type"] == notification_type]
|
||||
|
||||
|
||||
def get_monthly_usage_breakdown(year, monthly_usage, more_stats):
|
||||
sms = get_usage_breakdown_by_type(monthly_usage, 'sms')
|
||||
sms = get_usage_breakdown_by_type(monthly_usage, "sms")
|
||||
|
||||
for month in get_months_for_financial_year(year):
|
||||
monthly_sms = [row for row in sms if row['month'] == month]
|
||||
sms_free_allowance_used = sum(row['free_allowance_used'] for row in monthly_sms)
|
||||
sms_cost = sum(row['cost'] for row in monthly_sms)
|
||||
sms_breakdown = [row for row in monthly_sms if row['charged_units']]
|
||||
sms_counts = [row['sms_counts'] for row in more_stats if row['sms_counts'] and row['name'] == month]
|
||||
monthly_sms = [row for row in sms if row["month"] == month]
|
||||
sms_free_allowance_used = sum(row["free_allowance_used"] for row in monthly_sms)
|
||||
sms_cost = sum(row["cost"] for row in monthly_sms)
|
||||
sms_breakdown = [row for row in monthly_sms if row["charged_units"]]
|
||||
sms_counts = [
|
||||
row["sms_counts"]
|
||||
for row in more_stats
|
||||
if row["sms_counts"] and row["name"] == month
|
||||
]
|
||||
|
||||
yield {
|
||||
'month': month,
|
||||
'sms_free_allowance_used': sms_free_allowance_used,
|
||||
'sms_breakdown': sms_breakdown,
|
||||
'sms_cost': sms_cost,
|
||||
'sms_counts': sms_counts,
|
||||
"month": month,
|
||||
"sms_free_allowance_used": sms_free_allowance_used,
|
||||
"sms_breakdown": sms_breakdown,
|
||||
"sms_cost": sms_cost,
|
||||
"sms_counts": sms_counts,
|
||||
}
|
||||
|
||||
|
||||
def requested_and_current_financial_year(request):
|
||||
try:
|
||||
return (
|
||||
int(request.args.get('year', get_current_financial_year())),
|
||||
int(request.args.get("year", get_current_financial_year())),
|
||||
get_current_financial_year(),
|
||||
)
|
||||
except ValueError:
|
||||
@@ -422,10 +455,10 @@ def get_tuples_of_financial_years(
|
||||
):
|
||||
return (
|
||||
(
|
||||
'fiscal year',
|
||||
"fiscal year",
|
||||
year,
|
||||
partial_url(year=year),
|
||||
'{} to {}'.format(year, year + 1),
|
||||
"{} to {}".format(year, year + 1),
|
||||
)
|
||||
for year in reversed(range(start, end + 1))
|
||||
)
|
||||
|
||||
@@ -14,47 +14,55 @@ from app.s3_client.s3_logo_client import (
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
|
||||
@main.route("/email-branding", methods=['GET', 'POST'])
|
||||
@main.route("/email-branding", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def email_branding():
|
||||
brandings = email_branding_client.get_all_email_branding(sort_key='name')
|
||||
brandings = email_branding_client.get_all_email_branding(sort_key="name")
|
||||
|
||||
return render_template(
|
||||
'views/email-branding/select-branding.html',
|
||||
"views/email-branding/select-branding.html",
|
||||
email_brandings=brandings,
|
||||
search_form=SearchByNameForm()
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit", methods=['GET', 'POST'])
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit/<logo>", methods=['GET', 'POST'])
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit", methods=["GET", "POST"])
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit/<logo>", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def update_email_branding(branding_id, logo=None):
|
||||
email_branding = email_branding_client.get_email_branding(branding_id)['email_branding']
|
||||
email_branding = email_branding_client.get_email_branding(branding_id)[
|
||||
"email_branding"
|
||||
]
|
||||
|
||||
form = AdminEditEmailBrandingForm(
|
||||
name=email_branding['name'],
|
||||
text=email_branding['text'],
|
||||
colour=email_branding['colour'],
|
||||
brand_type=email_branding['brand_type']
|
||||
name=email_branding["name"],
|
||||
text=email_branding["text"],
|
||||
colour=email_branding["colour"],
|
||||
brand_type=email_branding["brand_type"],
|
||||
)
|
||||
|
||||
logo = logo if logo else email_branding.get('logo') if email_branding else None
|
||||
logo = logo if logo else email_branding.get("logo") if email_branding else None
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.file.data:
|
||||
upload_filename = upload_email_logo(
|
||||
form.file.data.filename,
|
||||
form.file.data,
|
||||
user_id=session["user_id"]
|
||||
form.file.data.filename, form.file.data, user_id=session["user_id"]
|
||||
)
|
||||
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session['user_id'])):
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
|
||||
delete_email_temp_file(logo)
|
||||
|
||||
return redirect(url_for('.update_email_branding', branding_id=branding_id, logo=upload_filename))
|
||||
return redirect(
|
||||
url_for(
|
||||
".update_email_branding",
|
||||
branding_id=branding_id,
|
||||
logo=upload_filename,
|
||||
)
|
||||
)
|
||||
|
||||
updated_logo_name = permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
updated_logo_name = (
|
||||
permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
)
|
||||
|
||||
email_branding_client.update_email_branding(
|
||||
branding_id=branding_id,
|
||||
@@ -70,37 +78,37 @@ def update_email_branding(branding_id, logo=None):
|
||||
|
||||
delete_email_temp_files_created_by(session["user_id"])
|
||||
|
||||
return redirect(url_for('.email_branding', branding_id=branding_id))
|
||||
return redirect(url_for(".email_branding", branding_id=branding_id))
|
||||
|
||||
return render_template(
|
||||
'views/email-branding/manage-branding.html',
|
||||
"views/email-branding/manage-branding.html",
|
||||
form=form,
|
||||
email_branding=email_branding,
|
||||
cdn_url=current_app.config['LOGO_CDN_DOMAIN'],
|
||||
logo=logo
|
||||
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
|
||||
logo=logo,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/email-branding/create", methods=['GET', 'POST'])
|
||||
@main.route("/email-branding/create/<logo>", methods=['GET', 'POST'])
|
||||
@main.route("/email-branding/create", methods=["GET", "POST"])
|
||||
@main.route("/email-branding/create/<logo>", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def create_email_branding(logo=None):
|
||||
form = AdminEditEmailBrandingForm(brand_type='org')
|
||||
form = AdminEditEmailBrandingForm(brand_type="org")
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.file.data:
|
||||
upload_filename = upload_email_logo(
|
||||
form.file.data.filename,
|
||||
form.file.data,
|
||||
user_id=session["user_id"]
|
||||
form.file.data.filename, form.file.data, user_id=session["user_id"]
|
||||
)
|
||||
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session['user_id'])):
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
|
||||
delete_email_temp_file(logo)
|
||||
|
||||
return redirect(url_for('.create_email_branding', logo=upload_filename))
|
||||
return redirect(url_for(".create_email_branding", logo=upload_filename))
|
||||
|
||||
updated_logo_name = permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
updated_logo_name = (
|
||||
permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
)
|
||||
|
||||
email_branding_client.create_email_branding(
|
||||
logo=updated_logo_name,
|
||||
@@ -115,11 +123,11 @@ def create_email_branding(logo=None):
|
||||
|
||||
delete_email_temp_files_created_by(session["user_id"])
|
||||
|
||||
return redirect(url_for('.email_branding'))
|
||||
return redirect(url_for(".email_branding"))
|
||||
|
||||
return render_template(
|
||||
'views/email-branding/manage-branding.html',
|
||||
"views/email-branding/manage-branding.html",
|
||||
form=form,
|
||||
cdn_url=current_app.config['LOGO_CDN_DOMAIN'],
|
||||
logo=logo
|
||||
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
|
||||
logo=logo,
|
||||
)
|
||||
|
||||
@@ -20,85 +20,86 @@ from app.utils import hide_from_search_engines
|
||||
bank_holidays = BankHolidays(use_cached_holidays=True)
|
||||
|
||||
|
||||
@main.route('/support', methods=['GET', 'POST'])
|
||||
@main.route("/support", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def support():
|
||||
|
||||
if current_user.is_authenticated:
|
||||
form = SupportType()
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for(
|
||||
'.feedback',
|
||||
ticket_type=form.support_type.data,
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".feedback",
|
||||
ticket_type=form.support_type.data,
|
||||
)
|
||||
)
|
||||
else:
|
||||
form = SupportRedirect()
|
||||
if form.validate_on_submit():
|
||||
if form.who.data == 'public':
|
||||
return redirect(url_for(
|
||||
'.support_public'
|
||||
))
|
||||
if form.who.data == "public":
|
||||
return redirect(url_for(".support_public"))
|
||||
else:
|
||||
return redirect(url_for(
|
||||
'.feedback',
|
||||
ticket_type=GENERAL_TICKET_TYPE,
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".feedback",
|
||||
ticket_type=GENERAL_TICKET_TYPE,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template('views/support/index.html', form=form)
|
||||
return render_template("views/support/index.html", form=form)
|
||||
|
||||
|
||||
@main.route('/support/public')
|
||||
@main.route("/support/public")
|
||||
@hide_from_search_engines
|
||||
def support_public():
|
||||
return render_template('views/support/public.html')
|
||||
return render_template("views/support/public.html")
|
||||
|
||||
|
||||
@main.route('/support/triage', methods=['GET', 'POST'])
|
||||
@main.route('/support/triage/<ticket_type:ticket_type>', methods=['GET', 'POST'])
|
||||
@main.route("/support/triage", methods=["GET", "POST"])
|
||||
@main.route("/support/triage/<ticket_type:ticket_type>", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def triage(ticket_type=PROBLEM_TICKET_TYPE):
|
||||
form = Triage()
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for(
|
||||
'.feedback',
|
||||
ticket_type=ticket_type,
|
||||
severe=form.severe.data
|
||||
))
|
||||
return redirect(
|
||||
url_for(".feedback", ticket_type=ticket_type, severe=form.severe.data)
|
||||
)
|
||||
return render_template(
|
||||
'views/support/triage.html',
|
||||
"views/support/triage.html",
|
||||
form=form,
|
||||
page_title={
|
||||
PROBLEM_TICKET_TYPE: 'Report a problem',
|
||||
GENERAL_TICKET_TYPE: 'Contact Notify.gov support',
|
||||
}.get(ticket_type)
|
||||
PROBLEM_TICKET_TYPE: "Report a problem",
|
||||
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
|
||||
}.get(ticket_type),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/support/<ticket_type:ticket_type>', methods=['GET', 'POST'])
|
||||
@main.route("/support/<ticket_type:ticket_type>", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def feedback(ticket_type):
|
||||
form = FeedbackOrProblem()
|
||||
|
||||
if not form.feedback.data:
|
||||
form.feedback.data = session.pop('feedback_message', '')
|
||||
form.feedback.data = session.pop("feedback_message", "")
|
||||
|
||||
if request.args.get('severe') in ['yes', 'no']:
|
||||
severe = convert_to_boolean(request.args.get('severe'))
|
||||
if request.args.get("severe") in ["yes", "no"]:
|
||||
severe = convert_to_boolean(request.args.get("severe"))
|
||||
else:
|
||||
severe = None
|
||||
|
||||
out_of_hours_emergency = all((
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
not in_business_hours(),
|
||||
severe,
|
||||
))
|
||||
out_of_hours_emergency = all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
not in_business_hours(),
|
||||
severe,
|
||||
)
|
||||
)
|
||||
|
||||
if needs_triage(ticket_type, severe):
|
||||
session['feedback_message'] = form.feedback.data
|
||||
return redirect(url_for('.triage', ticket_type=ticket_type))
|
||||
session["feedback_message"] = form.feedback.data
|
||||
return redirect(url_for(".triage", ticket_type=ticket_type))
|
||||
|
||||
if needs_escalation(ticket_type, severe):
|
||||
return redirect(url_for('.bat_phone'))
|
||||
return redirect(url_for(".bat_phone"))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
form.email_address.data = current_user.email_address
|
||||
@@ -109,12 +110,12 @@ def feedback(ticket_type):
|
||||
user_name = form.name.data or None
|
||||
|
||||
feedback_msg = render_template(
|
||||
'support-tickets/support-ticket.txt',
|
||||
"support-tickets/support-ticket.txt",
|
||||
content=form.feedback.data,
|
||||
)
|
||||
|
||||
ticket = NotifySupportTicket(
|
||||
subject='Notify feedback',
|
||||
subject="Notify feedback",
|
||||
message=feedback_msg,
|
||||
ticket_type=get_zendesk_ticket_type(ticket_type),
|
||||
p1=out_of_hours_emergency,
|
||||
@@ -126,54 +127,58 @@ def feedback(ticket_type):
|
||||
)
|
||||
zendesk_client.send_ticket_to_zendesk(ticket)
|
||||
|
||||
return redirect(url_for(
|
||||
'.thanks',
|
||||
out_of_hours_emergency=out_of_hours_emergency,
|
||||
email_address_provided=(
|
||||
current_user.is_authenticated or bool(form.email_address.data)
|
||||
),
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".thanks",
|
||||
out_of_hours_emergency=out_of_hours_emergency,
|
||||
email_address_provided=(
|
||||
current_user.is_authenticated or bool(form.email_address.data)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/support/form.html',
|
||||
"views/support/form.html",
|
||||
form=form,
|
||||
back_link=(
|
||||
url_for('.support')
|
||||
if severe is None else
|
||||
url_for('.triage', ticket_type=ticket_type)
|
||||
url_for(".support")
|
||||
if severe is None
|
||||
else url_for(".triage", ticket_type=ticket_type)
|
||||
),
|
||||
show_status_page_banner=(ticket_type == PROBLEM_TICKET_TYPE),
|
||||
page_title={
|
||||
GENERAL_TICKET_TYPE: 'Contact Notify.gov support',
|
||||
PROBLEM_TICKET_TYPE: 'Report a problem',
|
||||
QUESTION_TICKET_TYPE: 'Ask a question or give feedback',
|
||||
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
|
||||
PROBLEM_TICKET_TYPE: "Report a problem",
|
||||
QUESTION_TICKET_TYPE: "Ask a question or give feedback",
|
||||
}.get(ticket_type),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/support/escalate', methods=['GET', 'POST'])
|
||||
@main.route("/support/escalate", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def bat_phone():
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.feedback', ticket_type=PROBLEM_TICKET_TYPE))
|
||||
return redirect(url_for("main.feedback", ticket_type=PROBLEM_TICKET_TYPE))
|
||||
|
||||
return render_template('views/support/bat-phone.html')
|
||||
return render_template("views/support/bat-phone.html")
|
||||
|
||||
|
||||
@main.route('/support/thanks', methods=['GET', 'POST'])
|
||||
@main.route("/support/thanks", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def thanks():
|
||||
return render_template(
|
||||
'views/support/thanks.html',
|
||||
out_of_hours_emergency=convert_to_boolean(request.args.get('out_of_hours_emergency')),
|
||||
email_address_provided=convert_to_boolean(request.args.get('email_address_provided')),
|
||||
"views/support/thanks.html",
|
||||
out_of_hours_emergency=convert_to_boolean(
|
||||
request.args.get("out_of_hours_emergency")
|
||||
),
|
||||
email_address_provided=convert_to_boolean(
|
||||
request.args.get("email_address_provided")
|
||||
),
|
||||
out_of_hours=not in_business_hours(),
|
||||
)
|
||||
|
||||
|
||||
def in_business_hours():
|
||||
|
||||
now = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
|
||||
if is_weekend(now) or is_bank_holiday(now):
|
||||
@@ -183,15 +188,17 @@ def in_business_hours():
|
||||
|
||||
|
||||
def london_time_today_as_utc(hour, minute):
|
||||
return pytz.timezone('Europe/London').localize(
|
||||
datetime.now().replace(hour=hour, minute=minute)
|
||||
).astimezone(pytz.utc)
|
||||
return (
|
||||
pytz.timezone("Europe/London")
|
||||
.localize(datetime.now().replace(hour=hour, minute=minute))
|
||||
.astimezone(pytz.utc)
|
||||
)
|
||||
|
||||
|
||||
def is_weekend(time):
|
||||
return time.strftime('%A') in {
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
return time.strftime("%A") in {
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
}
|
||||
|
||||
|
||||
@@ -200,23 +207,25 @@ def is_bank_holiday(time):
|
||||
|
||||
|
||||
def needs_triage(ticket_type, severe):
|
||||
return all((
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe is None,
|
||||
return all(
|
||||
(
|
||||
not current_user.is_authenticated or current_user.live_services
|
||||
),
|
||||
not in_business_hours(),
|
||||
))
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe is None,
|
||||
(not current_user.is_authenticated or current_user.live_services),
|
||||
not in_business_hours(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def needs_escalation(ticket_type, severe):
|
||||
return all((
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe,
|
||||
not current_user.is_authenticated,
|
||||
not in_business_hours(),
|
||||
))
|
||||
return all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe,
|
||||
not current_user.is_authenticated,
|
||||
not in_business_hours(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_zendesk_ticket_type(ticket_type):
|
||||
|
||||
@@ -9,20 +9,23 @@ from app.main.forms import SearchByNameForm
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
|
||||
@main.route("/find-services-by-name", methods=['GET', 'POST'])
|
||||
@main.route("/find-services-by-name", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def find_services_by_name():
|
||||
form = SearchByNameForm()
|
||||
services_found = None
|
||||
if form.validate_on_submit():
|
||||
with suppress(ValueError):
|
||||
return redirect(url_for(
|
||||
'main.service_dashboard',
|
||||
service_id=uuid.UUID(form.search.data)
|
||||
))
|
||||
services_found = service_api_client.find_services_by_name(service_name=form.search.data)['data']
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.service_dashboard", service_id=uuid.UUID(form.search.data)
|
||||
)
|
||||
)
|
||||
services_found = service_api_client.find_services_by_name(
|
||||
service_name=form.search.data
|
||||
)["data"]
|
||||
return render_template(
|
||||
'views/find-services/find-services-by-name.html',
|
||||
"views/find-services/find-services-by-name.html",
|
||||
form=form,
|
||||
services_found=services_found
|
||||
services_found=services_found,
|
||||
)
|
||||
|
||||
@@ -10,49 +10,54 @@ from app.models.user import User
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
|
||||
@main.route("/find-users-by-email", methods=['GET', 'POST'])
|
||||
@main.route("/find-users-by-email", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def find_users_by_email():
|
||||
form = AdminSearchUsersByEmailForm()
|
||||
users_found = None
|
||||
if form.validate_on_submit():
|
||||
users_found = user_api_client.find_users_by_full_or_partial_email(form.search.data)['data']
|
||||
users_found = user_api_client.find_users_by_full_or_partial_email(
|
||||
form.search.data
|
||||
)["data"]
|
||||
return render_template(
|
||||
'views/find-users/find-users-by-email.html',
|
||||
form=form,
|
||||
users_found=users_found
|
||||
"views/find-users/find-users-by-email.html", form=form, users_found=users_found
|
||||
)
|
||||
|
||||
|
||||
@main.route("/users/<uuid:user_id>", methods=['GET'])
|
||||
@main.route("/users/<uuid:user_id>", methods=["GET"])
|
||||
@user_is_platform_admin
|
||||
def user_information(user_id):
|
||||
return render_template(
|
||||
'views/find-users/user-information.html',
|
||||
"views/find-users/user-information.html",
|
||||
user=User.from_id(user_id),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/users/<uuid:user_id>/archive", methods=['GET', 'POST'])
|
||||
@main.route("/users/<uuid:user_id>/archive", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def archive_user(user_id):
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
try:
|
||||
user_api_client.archive_user(user_id)
|
||||
except HTTPError as e:
|
||||
if e.status_code == 400 and 'manage_settings' in e.message:
|
||||
flash('User can’t be removed from a service - '
|
||||
'check all services have another team member with manage_settings')
|
||||
return redirect(url_for('main.user_information', user_id=user_id))
|
||||
if e.status_code == 400 and "manage_settings" in e.message:
|
||||
flash(
|
||||
"User can’t be removed from a service - "
|
||||
"check all services have another team member with manage_settings"
|
||||
)
|
||||
return redirect(url_for("main.user_information", user_id=user_id))
|
||||
create_archive_user_event(user_id=str(user_id), archived_by_id=current_user.id)
|
||||
|
||||
return redirect(url_for('.user_information', user_id=user_id))
|
||||
return redirect(url_for(".user_information", user_id=user_id))
|
||||
else:
|
||||
flash('There\'s no way to reverse this! Are you sure you want to archive this user?', 'delete')
|
||||
flash(
|
||||
"There's no way to reverse this! Are you sure you want to archive this user?",
|
||||
"delete",
|
||||
)
|
||||
return user_information(user_id)
|
||||
|
||||
|
||||
@main.route("/users/<uuid:user_id>/change_auth", methods=['GET', 'POST'])
|
||||
@main.route("/users/<uuid:user_id>/change_auth", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def change_user_auth(user_id):
|
||||
user = User.from_id(user_id)
|
||||
@@ -61,10 +66,10 @@ def change_user_auth(user_id):
|
||||
|
||||
if form.validate_on_submit():
|
||||
user.update(auth_type=form.auth_type.data)
|
||||
return redirect(url_for('.user_information', user_id=user_id))
|
||||
return redirect(url_for(".user_information", user_id=user_id))
|
||||
|
||||
return render_template(
|
||||
'views/find-users/auth_type.html',
|
||||
"views/find-users/auth_type.html",
|
||||
form=form,
|
||||
user=user,
|
||||
)
|
||||
|
||||
@@ -6,17 +6,19 @@ from app.main import main
|
||||
from app.main.forms import ForgotPasswordForm
|
||||
|
||||
|
||||
@main.route('/forgot-password', methods=['GET', 'POST'])
|
||||
@main.route("/forgot-password", methods=["GET", "POST"])
|
||||
def forgot_password():
|
||||
form = ForgotPasswordForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
user_api_client.send_reset_password_url(form.email_address.data, next_string=request.args.get('next'))
|
||||
user_api_client.send_reset_password_url(
|
||||
form.email_address.data, next_string=request.args.get("next")
|
||||
)
|
||||
except HTTPError as e:
|
||||
if e.status_code == 404:
|
||||
return render_template('views/password-reset-sent.html')
|
||||
return render_template("views/password-reset-sent.html")
|
||||
else:
|
||||
raise e
|
||||
return render_template('views/password-reset-sent.html')
|
||||
return render_template("views/password-reset-sent.html")
|
||||
|
||||
return render_template('views/forgot-password.html', form=form)
|
||||
return render_template("views/forgot-password.html", form=form)
|
||||
|
||||
@@ -10,34 +10,31 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/history")
|
||||
@user_has_permissions('manage_service')
|
||||
@user_has_permissions("manage_service")
|
||||
def history(service_id):
|
||||
|
||||
events = _get_events(current_service.id, request.args.get('selected'))
|
||||
events = _get_events(current_service.id, request.args.get("selected"))
|
||||
|
||||
return render_template(
|
||||
'views/temp-history.html',
|
||||
"views/temp-history.html",
|
||||
days=_chunk_events_by_day(events),
|
||||
show_navigation=request.args.get('selected') or any(
|
||||
isinstance(event, APIKeyEvent) for event in events
|
||||
),
|
||||
show_navigation=request.args.get("selected")
|
||||
or any(isinstance(event, APIKeyEvent) for event in events),
|
||||
user_getter=current_service.active_users.get_name_from_id,
|
||||
)
|
||||
|
||||
|
||||
def _get_events(service_id, selected):
|
||||
if selected == 'api':
|
||||
if selected == "api":
|
||||
return APIKeyEvents(service_id)
|
||||
if selected == 'service':
|
||||
if selected == "service":
|
||||
return ServiceEvents(service_id)
|
||||
return APIKeyEvents(service_id) + ServiceEvents(service_id)
|
||||
|
||||
|
||||
def _chunk_events_by_day(events):
|
||||
|
||||
days = defaultdict(list)
|
||||
|
||||
for event in sorted(events, key=attrgetter('time'), reverse=True):
|
||||
for event in sorted(events, key=attrgetter("time"), reverse=True):
|
||||
days[format_date_numeric(event.time)].append(event)
|
||||
|
||||
return sorted(days.items(), reverse=True)
|
||||
|
||||
@@ -5,9 +5,9 @@ from app.main import main
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
|
||||
@main.route('/inbound-sms-admin', methods=['GET', 'POST'])
|
||||
@main.route("/inbound-sms-admin", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def inbound_sms_admin():
|
||||
data = inbound_number_client.get_all_inbound_sms_number_service()
|
||||
|
||||
return render_template('views/inbound-sms-admin.html', inbound_num_list=data)
|
||||
return render_template("views/inbound-sms-admin.html", inbound_num_list=data)
|
||||
|
||||
@@ -18,64 +18,67 @@ from app.main.views.sub_navigation_dictionaries import features_nav, using_notif
|
||||
from app.utils.user import user_is_logged_in
|
||||
|
||||
|
||||
@main.route('/')
|
||||
@main.route("/")
|
||||
def index():
|
||||
|
||||
if current_user and current_user.is_authenticated:
|
||||
return redirect(url_for('main.choose_account'))
|
||||
return redirect(url_for("main.choose_account"))
|
||||
|
||||
return render_template(
|
||||
'views/signedout.html',
|
||||
"views/signedout.html",
|
||||
sms_rate=CURRENT_SMS_RATE,
|
||||
counts=status_api_client.get_count_of_live_services_and_organizations(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/error/<int:status_code>')
|
||||
@main.route("/error/<int:status_code>")
|
||||
def error(status_code):
|
||||
if status_code >= 500:
|
||||
abort(404)
|
||||
abort(status_code)
|
||||
|
||||
|
||||
@main.route('/privacy')
|
||||
@main.route("/privacy")
|
||||
@user_is_logged_in
|
||||
def privacy():
|
||||
return render_template('views/privacy.html')
|
||||
return render_template("views/privacy.html")
|
||||
|
||||
|
||||
@main.route('/accessibility-statement')
|
||||
@main.route("/accessibility-statement")
|
||||
@user_is_logged_in
|
||||
def accessibility_statement():
|
||||
return render_template('views/accessibility_statement.html')
|
||||
return render_template("views/accessibility_statement.html")
|
||||
|
||||
|
||||
@main.route('/delivery-and-failure')
|
||||
@main.route('/features/messages-status')
|
||||
@main.route("/delivery-and-failure")
|
||||
@main.route("/features/messages-status")
|
||||
def delivery_and_failure():
|
||||
return redirect(url_for('.message_status'), 301)
|
||||
return redirect(url_for(".message_status"), 301)
|
||||
|
||||
|
||||
@main.route('/design-patterns-content-guidance')
|
||||
@main.route("/design-patterns-content-guidance")
|
||||
@user_is_logged_in
|
||||
def design_content():
|
||||
return redirect('https://www.gov.uk/service-manual/design/sending-emails-and-text-messages', 301)
|
||||
return redirect(
|
||||
"https://www.gov.uk/service-manual/design/sending-emails-and-text-messages", 301
|
||||
)
|
||||
|
||||
|
||||
@main.route('/_email')
|
||||
@main.route("/_email")
|
||||
@user_is_logged_in
|
||||
def email_template():
|
||||
branding_type = 'govuk'
|
||||
branding_style = request.args.get('branding_style', None)
|
||||
branding_type = "govuk"
|
||||
branding_style = request.args.get("branding_style", None)
|
||||
|
||||
if branding_style == FieldWithNoneOption.NONE_OPTION_VALUE:
|
||||
branding_style = None
|
||||
|
||||
if branding_style is not None:
|
||||
email_branding = email_branding_client.get_email_branding(branding_style)['email_branding']
|
||||
branding_type = email_branding['brand_type']
|
||||
email_branding = email_branding_client.get_email_branding(branding_style)[
|
||||
"email_branding"
|
||||
]
|
||||
branding_type = email_branding["brand_type"]
|
||||
|
||||
if branding_type == 'govuk':
|
||||
if branding_type == "govuk":
|
||||
brand_text = None
|
||||
brand_colour = None
|
||||
brand_logo = None
|
||||
@@ -83,262 +86,260 @@ def email_template():
|
||||
brand_banner = False
|
||||
brand_name = None
|
||||
else:
|
||||
colour = email_branding['colour']
|
||||
brand_text = email_branding['text']
|
||||
colour = email_branding["colour"]
|
||||
brand_text = email_branding["text"]
|
||||
brand_colour = colour
|
||||
brand_logo = (f"https://{current_app.config['LOGO_CDN_DOMAIN']}/{email_branding['logo']}"
|
||||
if email_branding['logo'] else None)
|
||||
govuk_banner = branding_type in ['govuk', 'both']
|
||||
brand_banner = branding_type == 'org_banner'
|
||||
brand_name = email_branding['name']
|
||||
brand_logo = (
|
||||
f"https://{current_app.config['LOGO_CDN_DOMAIN']}/{email_branding['logo']}"
|
||||
if email_branding["logo"]
|
||||
else None
|
||||
)
|
||||
govuk_banner = branding_type in ["govuk", "both"]
|
||||
brand_banner = branding_type == "org_banner"
|
||||
brand_name = email_branding["name"]
|
||||
|
||||
template = {
|
||||
'template_type': 'email',
|
||||
'subject': 'Email branding preview',
|
||||
'content': (
|
||||
'Lorem Ipsum is simply dummy text of the printing and typesetting '
|
||||
'industry.\n\nLorem Ipsum has been the industry’s standard dummy '
|
||||
'text ever since the 1500s, when an unknown printer took a galley '
|
||||
'of type and scrambled it to make a type specimen book. '
|
||||
'\n\n'
|
||||
'# History'
|
||||
'\n\n'
|
||||
'It has '
|
||||
'survived not only'
|
||||
'\n\n'
|
||||
'* five centuries'
|
||||
'\n'
|
||||
'* but also the leap into electronic typesetting'
|
||||
'\n\n'
|
||||
'It was '
|
||||
'popularised in the 1960s with the release of Letraset sheets '
|
||||
'containing Lorem Ipsum passages, and more recently with desktop '
|
||||
'publishing software like Aldus PageMaker including versions of '
|
||||
'Lorem Ipsum.'
|
||||
'\n\n'
|
||||
'^ It is a long established fact that a reader will be distracted '
|
||||
'by the readable content of a page when looking at its layout.'
|
||||
'\n\n'
|
||||
'The point of using Lorem Ipsum is that it has a more-or-less '
|
||||
'normal distribution of letters, as opposed to using ‘Content '
|
||||
'here, content here’, making it look like readable English.'
|
||||
'\n\n\n'
|
||||
'1. One'
|
||||
'\n'
|
||||
'2. Two'
|
||||
'\n'
|
||||
'10. Three'
|
||||
'\n\n'
|
||||
'This is an example of an email sent using U.S. Notify.'
|
||||
'\n\n'
|
||||
'https://www.notifications.service.gov.uk'
|
||||
)
|
||||
"template_type": "email",
|
||||
"subject": "Email branding preview",
|
||||
"content": (
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting "
|
||||
"industry.\n\nLorem Ipsum has been the industry’s standard dummy "
|
||||
"text ever since the 1500s, when an unknown printer took a galley "
|
||||
"of type and scrambled it to make a type specimen book. "
|
||||
"\n\n"
|
||||
"# History"
|
||||
"\n\n"
|
||||
"It has "
|
||||
"survived not only"
|
||||
"\n\n"
|
||||
"* five centuries"
|
||||
"\n"
|
||||
"* but also the leap into electronic typesetting"
|
||||
"\n\n"
|
||||
"It was "
|
||||
"popularised in the 1960s with the release of Letraset sheets "
|
||||
"containing Lorem Ipsum passages, and more recently with desktop "
|
||||
"publishing software like Aldus PageMaker including versions of "
|
||||
"Lorem Ipsum."
|
||||
"\n\n"
|
||||
"^ It is a long established fact that a reader will be distracted "
|
||||
"by the readable content of a page when looking at its layout."
|
||||
"\n\n"
|
||||
"The point of using Lorem Ipsum is that it has a more-or-less "
|
||||
"normal distribution of letters, as opposed to using ‘Content "
|
||||
"here, content here’, making it look like readable English."
|
||||
"\n\n\n"
|
||||
"1. One"
|
||||
"\n"
|
||||
"2. Two"
|
||||
"\n"
|
||||
"10. Three"
|
||||
"\n\n"
|
||||
"This is an example of an email sent using U.S. Notify."
|
||||
"\n\n"
|
||||
"https://www.notifications.service.gov.uk"
|
||||
),
|
||||
}
|
||||
|
||||
if not bool(request.args):
|
||||
resp = make_response(str(HTMLEmailTemplate(template)))
|
||||
else:
|
||||
resp = make_response(str(HTMLEmailTemplate(
|
||||
template,
|
||||
govuk_banner=govuk_banner,
|
||||
brand_text=brand_text,
|
||||
brand_colour=brand_colour,
|
||||
brand_logo=brand_logo,
|
||||
brand_banner=brand_banner,
|
||||
brand_name=brand_name,
|
||||
)))
|
||||
resp = make_response(
|
||||
str(
|
||||
HTMLEmailTemplate(
|
||||
template,
|
||||
govuk_banner=govuk_banner,
|
||||
brand_text=brand_text,
|
||||
brand_colour=brand_colour,
|
||||
brand_logo=brand_logo,
|
||||
brand_banner=brand_banner,
|
||||
brand_name=brand_name,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
resp.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||
resp.headers["X-Frame-Options"] = "SAMEORIGIN"
|
||||
return resp
|
||||
|
||||
|
||||
@main.route('/documentation')
|
||||
@main.route("/documentation")
|
||||
@user_is_logged_in
|
||||
def documentation():
|
||||
return render_template(
|
||||
'views/documentation.html',
|
||||
"views/documentation.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/integration-testing')
|
||||
@main.route("/integration-testing")
|
||||
def integration_testing():
|
||||
return render_template('views/integration-testing.html'), 410
|
||||
return render_template("views/integration-testing.html"), 410
|
||||
|
||||
|
||||
@main.route('/callbacks')
|
||||
@main.route("/callbacks")
|
||||
def callbacks():
|
||||
return redirect(url_for('main.documentation'), 301)
|
||||
return redirect(url_for("main.documentation"), 301)
|
||||
|
||||
|
||||
# --- Features page set --- #
|
||||
|
||||
@main.route('/features')
|
||||
|
||||
@main.route("/features")
|
||||
@user_is_logged_in
|
||||
def features():
|
||||
return render_template(
|
||||
'views/features.html',
|
||||
navigation_links=features_nav()
|
||||
)
|
||||
return render_template("views/features.html", navigation_links=features_nav())
|
||||
|
||||
|
||||
@main.route('/features/roadmap', endpoint='roadmap')
|
||||
@main.route("/features/roadmap", endpoint="roadmap")
|
||||
@user_is_logged_in
|
||||
def roadmap():
|
||||
return render_template(
|
||||
'views/roadmap.html',
|
||||
navigation_links=features_nav()
|
||||
)
|
||||
return render_template("views/roadmap.html", navigation_links=features_nav())
|
||||
|
||||
|
||||
@main.route('/features/email')
|
||||
@main.route("/features/email")
|
||||
@user_is_logged_in
|
||||
def features_email():
|
||||
return render_template(
|
||||
'views/features/emails.html',
|
||||
navigation_links=features_nav()
|
||||
"views/features/emails.html", navigation_links=features_nav()
|
||||
)
|
||||
|
||||
|
||||
@main.route('/features/sms')
|
||||
@main.route("/features/sms")
|
||||
@user_is_logged_in
|
||||
def features_sms():
|
||||
return render_template(
|
||||
'views/features/text-messages.html',
|
||||
navigation_links=features_nav()
|
||||
"views/features/text-messages.html", navigation_links=features_nav()
|
||||
)
|
||||
|
||||
|
||||
@main.route('/features/security', endpoint='security')
|
||||
@main.route("/features/security", endpoint="security")
|
||||
@user_is_logged_in
|
||||
def security():
|
||||
return render_template(
|
||||
'views/security.html',
|
||||
navigation_links=features_nav()
|
||||
)
|
||||
return render_template("views/security.html", navigation_links=features_nav())
|
||||
|
||||
|
||||
@main.route('/features/terms', endpoint='terms')
|
||||
@main.route("/features/terms", endpoint="terms")
|
||||
@user_is_logged_in
|
||||
def terms():
|
||||
return render_template(
|
||||
'views/terms-of-use.html',
|
||||
"views/terms-of-use.html",
|
||||
navigation_links=features_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/features/using-notify')
|
||||
@main.route("/features/using-notify")
|
||||
@user_is_logged_in
|
||||
def using_notify():
|
||||
return render_template(
|
||||
'views/using-notify.html',
|
||||
navigation_links=features_nav()
|
||||
), 410
|
||||
return (
|
||||
render_template("views/using-notify.html", navigation_links=features_nav()),
|
||||
410,
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/delivery-status')
|
||||
@main.route("/using-notify/delivery-status")
|
||||
@user_is_logged_in
|
||||
def message_status():
|
||||
return render_template(
|
||||
'views/message-status.html',
|
||||
"views/message-status.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/features/get-started')
|
||||
@main.route("/features/get-started")
|
||||
@user_is_logged_in
|
||||
def get_started_old():
|
||||
return redirect(url_for('.get_started'), 301)
|
||||
return redirect(url_for(".get_started"), 301)
|
||||
|
||||
|
||||
@main.route('/using-notify/get-started')
|
||||
@main.route("/using-notify/get-started")
|
||||
@user_is_logged_in
|
||||
def get_started():
|
||||
return render_template(
|
||||
'views/get-started.html',
|
||||
"views/get-started.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/who-its-for')
|
||||
@main.route("/using-notify/who-its-for")
|
||||
def who_its_for():
|
||||
return redirect(url_for('.features'), 301)
|
||||
return redirect(url_for(".features"), 301)
|
||||
|
||||
|
||||
@main.route('/trial-mode')
|
||||
@main.route('/features/trial-mode')
|
||||
@main.route("/trial-mode")
|
||||
@main.route("/features/trial-mode")
|
||||
def trial_mode():
|
||||
return redirect(url_for('.trial_mode_new'), 301)
|
||||
return redirect(url_for(".trial_mode_new"), 301)
|
||||
|
||||
|
||||
@main.route('/using-notify/trial-mode')
|
||||
@main.route("/using-notify/trial-mode")
|
||||
def trial_mode_new():
|
||||
return render_template(
|
||||
'views/trial-mode.html',
|
||||
"views/trial-mode.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/guidance')
|
||||
@main.route("/using-notify/guidance")
|
||||
@user_is_logged_in
|
||||
def guidance_index():
|
||||
return render_template(
|
||||
'views/guidance/index.html',
|
||||
"views/guidance/index.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/guidance/branding-and-customisation')
|
||||
@main.route("/using-notify/guidance/branding-and-customisation")
|
||||
@user_is_logged_in
|
||||
def branding_and_customisation():
|
||||
return render_template(
|
||||
'views/guidance/branding-and-customisation.html',
|
||||
"views/guidance/branding-and-customisation.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/guidance/create-and-send-messages')
|
||||
@main.route("/using-notify/guidance/create-and-send-messages")
|
||||
@user_is_logged_in
|
||||
def create_and_send_messages():
|
||||
return render_template(
|
||||
'views/guidance/create-and-send-messages.html',
|
||||
"views/guidance/create-and-send-messages.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/guidance/edit-and-format-messages')
|
||||
@main.route("/using-notify/guidance/edit-and-format-messages")
|
||||
@user_is_logged_in
|
||||
def edit_and_format_messages():
|
||||
return render_template(
|
||||
'views/guidance/edit-and-format-messages.html',
|
||||
"views/guidance/edit-and-format-messages.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/guidance/send-files-by-email')
|
||||
@main.route("/using-notify/guidance/send-files-by-email")
|
||||
@user_is_logged_in
|
||||
def send_files_by_email():
|
||||
return render_template(
|
||||
'views/guidance/send-files-by-email.html',
|
||||
"views/guidance/send-files-by-email.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
# --- Redirects --- #
|
||||
|
||||
@main.route('/roadmap', endpoint='old_roadmap')
|
||||
@main.route('/terms', endpoint='old_terms')
|
||||
@main.route('/information-security', endpoint='information_security')
|
||||
@main.route('/using_notify', endpoint='old_using_notify')
|
||||
@main.route('/information-risk-management', endpoint='information_risk_management')
|
||||
@main.route('/integration_testing', endpoint='old_integration_testing')
|
||||
|
||||
@main.route("/roadmap", endpoint="old_roadmap")
|
||||
@main.route("/terms", endpoint="old_terms")
|
||||
@main.route("/information-security", endpoint="information_security")
|
||||
@main.route("/using_notify", endpoint="old_using_notify")
|
||||
@main.route("/information-risk-management", endpoint="information_risk_management")
|
||||
@main.route("/integration_testing", endpoint="old_integration_testing")
|
||||
def old_page_redirects():
|
||||
redirects = {
|
||||
'main.old_roadmap': 'main.roadmap',
|
||||
'main.old_terms': 'main.terms',
|
||||
'main.information_security': 'main.using_notify',
|
||||
'main.old_using_notify': 'main.using_notify',
|
||||
'main.information_risk_management': 'main.security',
|
||||
'main.old_integration_testing': 'main.integration_testing',
|
||||
"main.old_roadmap": "main.roadmap",
|
||||
"main.old_terms": "main.terms",
|
||||
"main.information_security": "main.using_notify",
|
||||
"main.old_using_notify": "main.using_notify",
|
||||
"main.information_risk_management": "main.security",
|
||||
"main.old_integration_testing": "main.integration_testing",
|
||||
}
|
||||
return redirect(url_for(redirects[request.endpoint]), code=301)
|
||||
|
||||
@@ -12,31 +12,40 @@ from app.models.user import InvitedOrgUser, InvitedUser, OrganizationUsers, User
|
||||
def accept_invite(token):
|
||||
invited_user = InvitedUser.from_token(token)
|
||||
|
||||
if not current_user.is_anonymous and current_user.email_address.lower() != invited_user.email_address.lower():
|
||||
message = Markup("""
|
||||
if (
|
||||
not current_user.is_anonymous
|
||||
and current_user.email_address.lower() != invited_user.email_address.lower()
|
||||
):
|
||||
message = Markup(
|
||||
"""
|
||||
You’re signed in as {}.
|
||||
This invite is for another email address.
|
||||
<a href={} class="usa-link">Sign out</a>
|
||||
and click the link again to accept this invite.
|
||||
""".format(
|
||||
current_user.email_address,
|
||||
url_for("main.sign_out")))
|
||||
current_user.email_address, url_for("main.sign_out")
|
||||
)
|
||||
)
|
||||
|
||||
flash(message=message)
|
||||
|
||||
abort(403)
|
||||
|
||||
if invited_user.status == 'cancelled':
|
||||
if invited_user.status == "cancelled":
|
||||
service = Service.from_id(invited_user.service)
|
||||
return render_template('views/cancelled-invitation.html',
|
||||
from_user=invited_user.from_user.name,
|
||||
service_name=service.name)
|
||||
if invited_user.status == 'accepted':
|
||||
session.pop('invited_user_id', None)
|
||||
return render_template(
|
||||
"views/cancelled-invitation.html",
|
||||
from_user=invited_user.from_user.name,
|
||||
service_name=service.name,
|
||||
)
|
||||
if invited_user.status == "accepted":
|
||||
session.pop("invited_user_id", None)
|
||||
service = Service.from_id(invited_user.service)
|
||||
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
|
||||
return redirect(
|
||||
url_for("main.service_dashboard", service_id=invited_user.service)
|
||||
)
|
||||
|
||||
session['invited_user_id'] = invited_user.id
|
||||
session["invited_user_id"] = invited_user.id
|
||||
|
||||
existing_user = User.from_email_address_or_none(invited_user.email_address)
|
||||
|
||||
@@ -44,17 +53,23 @@ def accept_invite(token):
|
||||
existing_user.update_email_access_validated_at()
|
||||
invited_user.accept_invite()
|
||||
if existing_user in Users(invited_user.service):
|
||||
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
|
||||
return redirect(
|
||||
url_for("main.service_dashboard", service_id=invited_user.service)
|
||||
)
|
||||
else:
|
||||
service = Service.from_id(invited_user.service)
|
||||
# if the service you're being added to can modify auth type, then check if we can do this;
|
||||
# if the user is a Platform Admin, we silently leave this unchanged to prevent a security
|
||||
# issue where someone could switch their auth type to something less secure
|
||||
if service.has_permission('email_auth') and not existing_user.platform_admin:
|
||||
if invited_user.auth_type == 'email_auth' or (
|
||||
if (
|
||||
service.has_permission("email_auth")
|
||||
and not existing_user.platform_admin
|
||||
):
|
||||
if invited_user.auth_type == "email_auth" or (
|
||||
# they have a phone number, we want them to start using it.
|
||||
# if they dont have a mobile we just ignore that option of the invite
|
||||
existing_user.mobile_number and invited_user.auth_type == 'sms_auth'
|
||||
existing_user.mobile_number
|
||||
and invited_user.auth_type == "sms_auth"
|
||||
):
|
||||
existing_user.update(auth_type=invited_user.auth_type)
|
||||
existing_user.add_to_service(
|
||||
@@ -63,40 +78,49 @@ def accept_invite(token):
|
||||
folder_permissions=invited_user.folder_permissions,
|
||||
invited_by_id=invited_user.from_user.id,
|
||||
)
|
||||
return redirect(url_for('main.service_dashboard', service_id=service.id))
|
||||
return redirect(url_for("main.service_dashboard", service_id=service.id))
|
||||
else:
|
||||
return redirect(url_for('main.register_from_invite'))
|
||||
return redirect(url_for("main.register_from_invite"))
|
||||
|
||||
|
||||
@main.route("/organization-invitation/<token>")
|
||||
def accept_org_invite(token):
|
||||
invited_org_user = InvitedOrgUser.from_token(token)
|
||||
|
||||
if not current_user.is_anonymous and current_user.email_address.lower() != invited_org_user.email_address.lower():
|
||||
message = Markup("""
|
||||
if (
|
||||
not current_user.is_anonymous
|
||||
and current_user.email_address.lower() != invited_org_user.email_address.lower()
|
||||
):
|
||||
message = Markup(
|
||||
"""
|
||||
You’re signed in as {}.
|
||||
This invite is for another email address.
|
||||
<a class="usa-link" href={}>Sign out</a>
|
||||
and click the link again to accept this invite.
|
||||
""".format(
|
||||
current_user.email_address,
|
||||
url_for("main.sign_out")))
|
||||
current_user.email_address, url_for("main.sign_out")
|
||||
)
|
||||
)
|
||||
|
||||
flash(message=message)
|
||||
|
||||
abort(403)
|
||||
|
||||
if invited_org_user.status == 'cancelled':
|
||||
if invited_org_user.status == "cancelled":
|
||||
organization = Organization.from_id(invited_org_user.organization)
|
||||
return render_template('views/cancelled-invitation.html',
|
||||
from_user=invited_org_user.invited_by.name,
|
||||
organization_name=organization.name)
|
||||
return render_template(
|
||||
"views/cancelled-invitation.html",
|
||||
from_user=invited_org_user.invited_by.name,
|
||||
organization_name=organization.name,
|
||||
)
|
||||
|
||||
if invited_org_user.status == 'accepted':
|
||||
session.pop('invited_org_user_id', None)
|
||||
return redirect(url_for('main.organization_dashboard', org_id=invited_org_user.organization))
|
||||
if invited_org_user.status == "accepted":
|
||||
session.pop("invited_org_user_id", None)
|
||||
return redirect(
|
||||
url_for("main.organization_dashboard", org_id=invited_org_user.organization)
|
||||
)
|
||||
|
||||
session['invited_org_user_id'] = invited_org_user.id
|
||||
session["invited_org_user_id"] = invited_org_user.id
|
||||
|
||||
existing_user = User.from_email_address_or_none(invited_org_user.email_address)
|
||||
organization_users = OrganizationUsers(invited_org_user.organization)
|
||||
@@ -105,7 +129,11 @@ def accept_org_invite(token):
|
||||
existing_user.update_email_access_validated_at()
|
||||
invited_org_user.accept_invite()
|
||||
if existing_user not in organization_users:
|
||||
existing_user.add_to_organization(organization_id=invited_org_user.organization)
|
||||
return redirect(url_for('main.organization_dashboard', org_id=invited_org_user.organization))
|
||||
existing_user.add_to_organization(
|
||||
organization_id=invited_org_user.organization
|
||||
)
|
||||
return redirect(
|
||||
url_for("main.organization_dashboard", org_id=invited_org_user.organization)
|
||||
)
|
||||
else:
|
||||
return redirect(url_for('main.register_from_org_invite'))
|
||||
return redirect(url_for("main.register_from_org_invite"))
|
||||
|
||||
@@ -39,10 +39,12 @@ from app.utils.user import user_has_permissions
|
||||
@main.route("/services/<uuid:service_id>/jobs")
|
||||
@user_has_permissions()
|
||||
def view_jobs(service_id):
|
||||
return redirect(url_for(
|
||||
'main.uploads',
|
||||
service_id=current_service.id,
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.uploads",
|
||||
service_id=current_service.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>")
|
||||
@@ -53,205 +55,215 @@ def view_job(service_id, job_id):
|
||||
abort(404)
|
||||
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args['status'] = set_status_filters(filter_args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
|
||||
return render_template(
|
||||
'views/jobs/job.html',
|
||||
"views/jobs/job.html",
|
||||
job=job,
|
||||
status=request.args.get('status', ''),
|
||||
status=request.args.get("status", ""),
|
||||
updates_url=url_for(
|
||||
".view_job_updates",
|
||||
service_id=service_id,
|
||||
job_id=job.id,
|
||||
status=request.args.get('status', ''),
|
||||
status=request.args.get("status", ""),
|
||||
),
|
||||
partials=get_job_partials(job),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>.csv")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def view_job_csv(service_id, job_id):
|
||||
job = Job.from_id(job_id, service_id=service_id)
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args['status'] = set_status_filters(filter_args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
|
||||
return Response(
|
||||
stream_with_context(
|
||||
generate_notifications_csv(
|
||||
service_id=service_id,
|
||||
job_id=job_id,
|
||||
status=filter_args.get('status'),
|
||||
page=request.args.get('page', 1),
|
||||
status=filter_args.get("status"),
|
||||
page=request.args.get("page", 1),
|
||||
page_size=5000,
|
||||
format_for_csv=True,
|
||||
template_type=job.template_type,
|
||||
)
|
||||
),
|
||||
mimetype='text/csv',
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
'Content-Disposition': 'inline; filename="{} - {}.csv"'.format(
|
||||
job.template['name'],
|
||||
format_datetime_short(job.created_at)
|
||||
"Content-Disposition": 'inline; filename="{} - {}.csv"'.format(
|
||||
job.template["name"], format_datetime_short(job.created_at)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>", methods=['POST'])
|
||||
@user_has_permissions('send_messages')
|
||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages")
|
||||
def cancel_job(service_id, job_id):
|
||||
Job.from_id(job_id, service_id=service_id).cancel()
|
||||
return redirect(url_for('main.service_dashboard', service_id=service_id))
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>.json")
|
||||
@user_has_permissions()
|
||||
def view_job_updates(service_id, job_id):
|
||||
|
||||
job = Job.from_id(job_id, service_id=service_id)
|
||||
|
||||
return jsonify(**get_job_partials(job))
|
||||
|
||||
|
||||
@main.route('/services/<uuid:service_id>/notifications', methods=['GET', 'POST'])
|
||||
@main.route('/services/<uuid:service_id>/notifications/<template_type:message_type>', methods=['GET', 'POST'])
|
||||
@main.route("/services/<uuid:service_id>/notifications", methods=["GET", "POST"])
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/notifications/<template_type:message_type>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions()
|
||||
def view_notifications(service_id, message_type=None):
|
||||
return render_template(
|
||||
'views/notifications.html',
|
||||
"views/notifications.html",
|
||||
partials=get_notifications(service_id, message_type),
|
||||
message_type=message_type,
|
||||
status=request.args.get('status') or 'sending,delivered,failed',
|
||||
page=request.args.get('page', 1),
|
||||
status=request.args.get("status") or "sending,delivered,failed",
|
||||
page=request.args.get("page", 1),
|
||||
search_form=SearchNotificationsForm(
|
||||
message_type=message_type,
|
||||
to=request.form.get('to'),
|
||||
to=request.form.get("to"),
|
||||
),
|
||||
things_you_can_search_by={
|
||||
'email': ['email address'],
|
||||
'sms': ['phone number'],
|
||||
"email": ["email address"],
|
||||
"sms": ["phone number"],
|
||||
# We say recipient here because combining all 3 types, plus
|
||||
# reference gets too long for the hint text
|
||||
None: ['recipient'],
|
||||
}.get(message_type) + {
|
||||
True: ['reference'],
|
||||
None: ["recipient"],
|
||||
}.get(message_type)
|
||||
+ {
|
||||
True: ["reference"],
|
||||
False: [],
|
||||
}.get(bool(current_service.api_keys)),
|
||||
download_link=url_for(
|
||||
'.download_notifications_csv',
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get('status')
|
||||
status=request.args.get("status"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/notifications.json", methods=["GET", "POST"])
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/notifications/<template_type:message_type>.json",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions()
|
||||
def get_notifications_as_json(service_id, message_type=None):
|
||||
return jsonify(
|
||||
get_notifications(
|
||||
service_id, message_type, status_override=request.args.get("status")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@main.route('/services/<uuid:service_id>/notifications.json', methods=['GET', 'POST'])
|
||||
@main.route('/services/<uuid:service_id>/notifications/<template_type:message_type>.json', methods=['GET', 'POST'])
|
||||
@user_has_permissions()
|
||||
def get_notifications_as_json(service_id, message_type=None):
|
||||
return jsonify(get_notifications(
|
||||
service_id, message_type, status_override=request.args.get('status')
|
||||
))
|
||||
|
||||
|
||||
@main.route('/services/<uuid:service_id>/notifications.csv', endpoint="view_notifications_csv")
|
||||
@main.route(
|
||||
'/services/<uuid:service_id>/notifications/<template_type:message_type>.csv',
|
||||
endpoint="view_notifications_csv"
|
||||
"/services/<uuid:service_id>/notifications.csv", endpoint="view_notifications_csv"
|
||||
)
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/notifications/<template_type:message_type>.csv",
|
||||
endpoint="view_notifications_csv",
|
||||
)
|
||||
@user_has_permissions()
|
||||
def get_notifications(service_id, message_type, status_override=None): # noqa
|
||||
def get_notifications(service_id, message_type, status_override=None): # noqa
|
||||
# TODO get the api to return count of pages as well.
|
||||
page = get_page_from_request()
|
||||
if page is None:
|
||||
abort(404, "Invalid page argument ({}).".format(request.args.get('page')))
|
||||
abort(404, "Invalid page argument ({}).".format(request.args.get("page")))
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args['status'] = set_status_filters(filter_args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
service_data_retention_days = None
|
||||
search_term = request.form.get('to', '')
|
||||
search_term = request.form.get("to", "")
|
||||
|
||||
if message_type is not None:
|
||||
service_data_retention_days = current_service.get_days_of_retention(message_type)
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
message_type
|
||||
)
|
||||
|
||||
if request.path.endswith('csv') and current_user.has_permissions('view_activity'):
|
||||
if request.path.endswith("csv") and current_user.has_permissions("view_activity"):
|
||||
return Response(
|
||||
generate_notifications_csv(
|
||||
service_id=service_id,
|
||||
page=page,
|
||||
page_size=5000,
|
||||
template_type=[message_type],
|
||||
status=filter_args.get('status'),
|
||||
limit_days=service_data_retention_days
|
||||
status=filter_args.get("status"),
|
||||
limit_days=service_data_retention_days,
|
||||
),
|
||||
mimetype='text/csv',
|
||||
headers={
|
||||
'Content-Disposition': 'inline; filename="notifications.csv"'}
|
||||
mimetype="text/csv",
|
||||
headers={"Content-Disposition": 'inline; filename="notifications.csv"'},
|
||||
)
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
service_id=service_id,
|
||||
page=page,
|
||||
template_type=[message_type] if message_type else [],
|
||||
status=filter_args.get('status'),
|
||||
status=filter_args.get("status"),
|
||||
limit_days=service_data_retention_days,
|
||||
to=search_term,
|
||||
)
|
||||
url_args = {
|
||||
'message_type': message_type,
|
||||
'status': request.args.get('status')
|
||||
}
|
||||
url_args = {"message_type": message_type, "status": request.args.get("status")}
|
||||
prev_page = None
|
||||
|
||||
if 'links' in notifications and notifications['links'].get('prev', None):
|
||||
prev_page = generate_previous_dict('main.view_notifications', service_id, page, url_args=url_args)
|
||||
if "links" in notifications and notifications["links"].get("prev", None):
|
||||
prev_page = generate_previous_dict(
|
||||
"main.view_notifications", service_id, page, url_args=url_args
|
||||
)
|
||||
next_page = None
|
||||
|
||||
if 'links' in notifications and notifications['links'].get('next', None):
|
||||
next_page = generate_next_dict('main.view_notifications', service_id, page, url_args)
|
||||
if "links" in notifications and notifications["links"].get("next", None):
|
||||
next_page = generate_next_dict(
|
||||
"main.view_notifications", service_id, page, url_args
|
||||
)
|
||||
|
||||
if message_type:
|
||||
download_link = url_for(
|
||||
'.view_notifications_csv',
|
||||
".view_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get('status')
|
||||
status=request.args.get("status"),
|
||||
)
|
||||
else:
|
||||
download_link = None
|
||||
|
||||
return {
|
||||
'service_data_retention_days': service_data_retention_days,
|
||||
'counts': render_template(
|
||||
'views/activity/counts.html',
|
||||
status=request.args.get('status'),
|
||||
"service_data_retention_days": service_data_retention_days,
|
||||
"counts": render_template(
|
||||
"views/activity/counts.html",
|
||||
status=request.args.get("status"),
|
||||
status_filters=get_status_filters(
|
||||
current_service,
|
||||
message_type,
|
||||
service_api_client.get_service_statistics(
|
||||
service_id,
|
||||
limit_days=service_data_retention_days
|
||||
)
|
||||
)
|
||||
service_id, limit_days=service_data_retention_days
|
||||
),
|
||||
),
|
||||
),
|
||||
'notifications': render_template(
|
||||
'views/activity/notifications.html',
|
||||
notifications=list(add_preview_of_content_to_notifications(
|
||||
notifications['notifications']
|
||||
)),
|
||||
"notifications": render_template(
|
||||
"views/activity/notifications.html",
|
||||
notifications=list(
|
||||
add_preview_of_content_to_notifications(notifications["notifications"])
|
||||
),
|
||||
page=page,
|
||||
limit_days=service_data_retention_days,
|
||||
prev_page=prev_page,
|
||||
next_page=next_page,
|
||||
show_pagination=(not search_term),
|
||||
status=request.args.get('status'),
|
||||
status=request.args.get("status"),
|
||||
message_type=message_type,
|
||||
download_link=download_link,
|
||||
single_notification_url=partial(
|
||||
url_for,
|
||||
'.view_notification',
|
||||
".view_notification",
|
||||
service_id=current_service.id,
|
||||
)
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -259,22 +271,19 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
|
||||
def get_status_filters(service, message_type, statistics):
|
||||
if message_type is None:
|
||||
stats = {
|
||||
key: sum(
|
||||
statistics[message_type][key]
|
||||
for message_type in {'email', 'sms'}
|
||||
)
|
||||
for key in {'requested', 'delivered', 'failed'}
|
||||
key: sum(statistics[message_type][key] for message_type in {"email", "sms"})
|
||||
for key in {"requested", "delivered", "failed"}
|
||||
}
|
||||
else:
|
||||
stats = statistics[message_type]
|
||||
stats['sending'] = stats['requested'] - stats['delivered'] - stats['failed']
|
||||
stats["sending"] = stats["requested"] - stats["delivered"] - stats["failed"]
|
||||
|
||||
filters = [
|
||||
# key, label, option
|
||||
('requested', 'total', 'sending,delivered,failed'),
|
||||
('sending', 'pending', 'pending'),
|
||||
('delivered', 'delivered', 'delivered'),
|
||||
('failed', 'failed', 'failed'),
|
||||
("requested", "total", "sending,delivered,failed"),
|
||||
("sending", "pending", "pending"),
|
||||
("delivered", "delivered", "delivered"),
|
||||
("failed", "failed", "failed"),
|
||||
]
|
||||
return [
|
||||
# return list containing label, option, link, count
|
||||
@@ -282,12 +291,12 @@ def get_status_filters(service, message_type, statistics):
|
||||
label,
|
||||
option,
|
||||
url_for(
|
||||
'.view_notifications',
|
||||
".view_notifications",
|
||||
service_id=service.id,
|
||||
message_type=message_type,
|
||||
status=option
|
||||
status=option,
|
||||
),
|
||||
stats[key]
|
||||
stats[key],
|
||||
)
|
||||
for key, label, option in filters
|
||||
]
|
||||
@@ -305,106 +314,114 @@ def _get_job_counts(job):
|
||||
job_id=job.id,
|
||||
status=query_param,
|
||||
),
|
||||
count
|
||||
) for label, query_param, count in [
|
||||
count,
|
||||
)
|
||||
for label, query_param, count in [
|
||||
[
|
||||
Markup(
|
||||
f'''total<span class="usa-sr-only">
|
||||
{"text message" if job_type == "sms" else job_type}s</span>'''
|
||||
f"""total<span class="usa-sr-only">
|
||||
{"text message" if job_type == "sms" else job_type}s</span>"""
|
||||
),
|
||||
'',
|
||||
job.notification_count
|
||||
"",
|
||||
job.notification_count,
|
||||
],
|
||||
[
|
||||
Markup(
|
||||
f'''pending<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_sending, job_type)}</span>'''
|
||||
f"""pending<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_sending, job_type)}</span>"""
|
||||
),
|
||||
'pending',
|
||||
job.notifications_sending
|
||||
"pending",
|
||||
job.notifications_sending,
|
||||
],
|
||||
[
|
||||
Markup(
|
||||
f'''delivered<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_delivered, job_type)}</span>'''
|
||||
f"""delivered<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_delivered, job_type)}</span>"""
|
||||
),
|
||||
'delivered',
|
||||
job.notifications_delivered
|
||||
"delivered",
|
||||
job.notifications_delivered,
|
||||
],
|
||||
[
|
||||
Markup(
|
||||
f'''failed<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_failed, job_type)}</span>'''
|
||||
f"""failed<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_failed, job_type)}</span>"""
|
||||
),
|
||||
'failed',
|
||||
job.notifications_failed
|
||||
]
|
||||
"failed",
|
||||
job.notifications_failed,
|
||||
],
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def get_job_partials(job):
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args['status'] = set_status_filters(filter_args)
|
||||
notifications = job.get_notifications(status=filter_args['status'])
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
notifications = job.get_notifications(status=filter_args["status"])
|
||||
counts = render_template(
|
||||
'partials/count.html',
|
||||
"partials/count.html",
|
||||
counts=_get_job_counts(job),
|
||||
status=filter_args['status'],
|
||||
status=filter_args["status"],
|
||||
notifications_deleted=(
|
||||
job.status == 'finished' and not notifications['notifications']
|
||||
job.status == "finished" and not notifications["notifications"]
|
||||
),
|
||||
)
|
||||
service_data_retention_days = current_service.get_days_of_retention(job.template_type)
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
job.template_type
|
||||
)
|
||||
|
||||
return {
|
||||
'counts': counts,
|
||||
'notifications': render_template(
|
||||
'partials/jobs/notifications.html',
|
||||
"counts": counts,
|
||||
"notifications": render_template(
|
||||
"partials/jobs/notifications.html",
|
||||
notifications=list(
|
||||
add_preview_of_content_to_notifications(notifications['notifications'])
|
||||
add_preview_of_content_to_notifications(notifications["notifications"])
|
||||
),
|
||||
more_than_one_page=bool(notifications.get('links', {}).get('next')),
|
||||
more_than_one_page=bool(notifications.get("links", {}).get("next")),
|
||||
download_link=url_for(
|
||||
'.view_job_csv',
|
||||
".view_job_csv",
|
||||
service_id=current_service.id,
|
||||
job_id=job.id,
|
||||
status=request.args.get('status')
|
||||
status=request.args.get("status"),
|
||||
),
|
||||
time_left=get_time_left(
|
||||
job.created_at, service_data_retention_days=service_data_retention_days
|
||||
),
|
||||
time_left=get_time_left(job.created_at, service_data_retention_days=service_data_retention_days),
|
||||
job=job,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
),
|
||||
'status': render_template(
|
||||
'partials/jobs/status.html',
|
||||
"status": render_template(
|
||||
"partials/jobs/status.html",
|
||||
job=job,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def add_preview_of_content_to_notifications(notifications):
|
||||
|
||||
for notification in notifications:
|
||||
yield (dict(
|
||||
preview_of_content=get_preview_of_content(notification),
|
||||
**notification
|
||||
))
|
||||
yield (
|
||||
dict(
|
||||
preview_of_content=get_preview_of_content(notification), **notification
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_preview_of_content(notification):
|
||||
if notification["template"].get("redact_personalisation"):
|
||||
notification["personalisation"] = {}
|
||||
|
||||
if notification['template'].get('redact_personalisation'):
|
||||
notification['personalisation'] = {}
|
||||
if notification["template"]["template_type"] == "sms":
|
||||
return str(
|
||||
SMSBodyPreviewTemplate(
|
||||
notification["template"],
|
||||
notification["personalisation"],
|
||||
)
|
||||
)
|
||||
|
||||
if notification['template']['template_type'] == 'sms':
|
||||
return str(SMSBodyPreviewTemplate(
|
||||
notification['template'],
|
||||
notification['personalisation'],
|
||||
))
|
||||
|
||||
if notification['template']['template_type'] == 'email':
|
||||
return Markup(EmailPreviewTemplate(
|
||||
notification['template'],
|
||||
notification['personalisation'],
|
||||
redact_missing_personalisation=True,
|
||||
).subject)
|
||||
if notification["template"]["template_type"] == "email":
|
||||
return Markup(
|
||||
EmailPreviewTemplate(
|
||||
notification["template"],
|
||||
notification["personalisation"],
|
||||
redact_missing_personalisation=True,
|
||||
).subject
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ from app.utils.user_permissions import permission_options
|
||||
@user_has_permissions(allow_org_user=True)
|
||||
def manage_users(service_id):
|
||||
return render_template(
|
||||
'views/manage-users.html',
|
||||
"views/manage-users.html",
|
||||
users=current_service.team_members,
|
||||
current_user=current_user,
|
||||
show_search_box=(len(current_service.team_members) > 7),
|
||||
@@ -38,29 +38,30 @@ def manage_users(service_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/invite", methods=['GET', 'POST'])
|
||||
@main.route("/services/<uuid:service_id>/users/invite/<uuid:user_id>", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route("/services/<uuid:service_id>/users/invite", methods=["GET", "POST"])
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/users/invite/<uuid:user_id>", methods=["GET", "POST"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def invite_user(service_id, user_id=None):
|
||||
|
||||
form_class = InviteUserForm
|
||||
|
||||
form = form_class(
|
||||
inviter_email_address=current_user.email_address,
|
||||
all_template_folders=current_service.all_template_folders,
|
||||
folder_permissions=[f['id'] for f in current_service.all_template_folders]
|
||||
folder_permissions=[f["id"] for f in current_service.all_template_folders],
|
||||
)
|
||||
|
||||
if user_id:
|
||||
user_to_invite = User.from_id(user_id)
|
||||
if user_to_invite.belongs_to_service(current_service.id):
|
||||
return render_template(
|
||||
'views/user-already-team-member.html',
|
||||
"views/user-already-team-member.html",
|
||||
user_to_invite=user_to_invite,
|
||||
)
|
||||
if current_service.invite_pending_for(user_to_invite.email_address):
|
||||
return render_template(
|
||||
'views/user-already-invited.html',
|
||||
"views/user-already-invited.html",
|
||||
user_to_invite=user_to_invite,
|
||||
)
|
||||
if not user_to_invite.default_organization:
|
||||
@@ -71,9 +72,9 @@ def invite_user(service_id, user_id=None):
|
||||
else:
|
||||
user_to_invite = None
|
||||
|
||||
service_has_email_auth = current_service.has_permission('email_auth')
|
||||
service_has_email_auth = current_service.has_permission("email_auth")
|
||||
if not service_has_email_auth:
|
||||
form.login_authentication.data = 'sms_auth'
|
||||
form.login_authentication.data = "sms_auth"
|
||||
|
||||
if form.validate_on_submit():
|
||||
email_address = form.email_address.data
|
||||
@@ -92,11 +93,13 @@ def invite_user(service_id, user_id=None):
|
||||
ui_permissions=form.permissions,
|
||||
)
|
||||
|
||||
flash('Invite sent to {}'.format(invited_user.email_address), 'default_with_tick')
|
||||
return redirect(url_for('.manage_users', service_id=service_id))
|
||||
flash(
|
||||
"Invite sent to {}".format(invited_user.email_address), "default_with_tick"
|
||||
)
|
||||
return redirect(url_for(".manage_users", service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
'views/invite-user.html',
|
||||
"views/invite-user.html",
|
||||
form=form,
|
||||
service_has_email_auth=service_has_email_auth,
|
||||
mobile_number=True,
|
||||
@@ -104,10 +107,10 @@ def invite_user(service_id, user_id=None):
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>", methods=["GET", "POST"])
|
||||
@user_has_permissions("manage_service")
|
||||
def edit_user_permissions(service_id, user_id):
|
||||
service_has_email_auth = current_service.has_permission('email_auth')
|
||||
service_has_email_auth = current_service.has_permission("email_auth")
|
||||
user = current_service.get_team_member(user_id)
|
||||
|
||||
mobile_number = None
|
||||
@@ -119,11 +122,16 @@ def edit_user_permissions(service_id, user_id):
|
||||
form = form_class.from_user(
|
||||
user,
|
||||
service_id,
|
||||
folder_permissions=None if user.platform_admin else [
|
||||
f['id'] for f in current_service.all_template_folders
|
||||
folder_permissions=None
|
||||
if user.platform_admin
|
||||
else [
|
||||
f["id"]
|
||||
for f in current_service.all_template_folders
|
||||
if user.has_template_folder_permission(f)
|
||||
],
|
||||
all_template_folders=None if user.platform_admin else current_service.all_template_folders
|
||||
all_template_folders=None
|
||||
if user.platform_admin
|
||||
else current_service.all_template_folders,
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
@@ -136,87 +144,86 @@ def edit_user_permissions(service_id, user_id):
|
||||
# Only change the auth type if this is supported for a service.
|
||||
if service_has_email_auth:
|
||||
user.update(auth_type=form.login_authentication.data)
|
||||
return redirect(url_for('.manage_users', service_id=service_id))
|
||||
return redirect(url_for(".manage_users", service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
'views/edit-user-permissions.html',
|
||||
"views/edit-user-permissions.html",
|
||||
user=user,
|
||||
form=form,
|
||||
service_has_email_auth=service_has_email_auth,
|
||||
mobile_number=mobile_number,
|
||||
delete=request.args.get('delete'),
|
||||
delete=request.args.get("delete"),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/delete", methods=['POST'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/delete", methods=["POST"])
|
||||
@user_has_permissions("manage_service")
|
||||
def remove_user_from_service(service_id, user_id):
|
||||
try:
|
||||
service_api_client.remove_user_from_service(service_id, user_id)
|
||||
except HTTPError as e:
|
||||
msg = "You cannot remove the only user for a service"
|
||||
if e.status_code == 400 and msg in e.message:
|
||||
flash(msg, 'info')
|
||||
return redirect(url_for(
|
||||
'.manage_users',
|
||||
service_id=service_id))
|
||||
flash(msg, "info")
|
||||
return redirect(url_for(".manage_users", service_id=service_id))
|
||||
else:
|
||||
abort(500, e)
|
||||
else:
|
||||
create_remove_user_from_service_event(
|
||||
user_id=user_id,
|
||||
removed_by_id=current_user.id,
|
||||
service_id=service_id
|
||||
user_id=user_id, removed_by_id=current_user.id, service_id=service_id
|
||||
)
|
||||
|
||||
return redirect(url_for(
|
||||
'.manage_users',
|
||||
service_id=service_id
|
||||
))
|
||||
return redirect(url_for(".manage_users", service_id=service_id))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/edit-email", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def edit_user_email(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
user_email = user.email_address
|
||||
session_key = 'team_member_email_change-{}'.format(user_id)
|
||||
session_key = "team_member_email_change-{}".format(user_id)
|
||||
|
||||
if is_gov_user(user_email):
|
||||
form = ChangeEmailForm(User.already_registered, email_address=user_email)
|
||||
else:
|
||||
form = ChangeNonGovEmailForm(User.already_registered, email_address=user_email)
|
||||
|
||||
if request.form.get('email_address', '').strip() == user_email:
|
||||
return redirect(url_for('.manage_users', service_id=current_service.id))
|
||||
if request.form.get("email_address", "").strip() == user_email:
|
||||
return redirect(url_for(".manage_users", service_id=current_service.id))
|
||||
|
||||
if form.validate_on_submit():
|
||||
session[session_key] = form.email_address.data
|
||||
|
||||
return redirect(url_for('.confirm_edit_user_email', user_id=user.id, service_id=service_id))
|
||||
return redirect(
|
||||
url_for(".confirm_edit_user_email", user_id=user.id, service_id=service_id)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/manage-users/edit-user-email.html',
|
||||
"views/manage-users/edit-user-email.html",
|
||||
user=user,
|
||||
form=form,
|
||||
service_id=service_id
|
||||
service_id=service_id,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/edit-email/confirm", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email/confirm",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def confirm_edit_user_email(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
session_key = 'team_member_email_change-{}'.format(user_id)
|
||||
session_key = "team_member_email_change-{}".format(user_id)
|
||||
if session_key in session:
|
||||
new_email = session[session_key]
|
||||
else:
|
||||
return redirect(url_for(
|
||||
'.edit_user_email',
|
||||
service_id=service_id,
|
||||
user_id=user_id
|
||||
))
|
||||
if request.method == 'POST':
|
||||
return redirect(
|
||||
url_for(".edit_user_email", service_id=service_id, user_id=user_id)
|
||||
)
|
||||
if request.method == "POST":
|
||||
try:
|
||||
user.update(email_address=new_email, updated_by=current_user.id)
|
||||
except HTTPError as e:
|
||||
@@ -226,60 +233,64 @@ def confirm_edit_user_email(service_id, user_id):
|
||||
user_id=user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_email_address=user.email_address,
|
||||
new_email_address=new_email
|
||||
new_email_address=new_email,
|
||||
)
|
||||
finally:
|
||||
session.pop(session_key, None)
|
||||
|
||||
return redirect(url_for(
|
||||
'.manage_users',
|
||||
service_id=service_id
|
||||
))
|
||||
return redirect(url_for(".manage_users", service_id=service_id))
|
||||
return render_template(
|
||||
'views/manage-users/confirm-edit-user-email.html',
|
||||
"views/manage-users/confirm-edit-user-email.html",
|
||||
user=user,
|
||||
service_id=service_id,
|
||||
new_email=new_email
|
||||
new_email=new_email,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def edit_user_mobile_number(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
user_mobile_number = redact_mobile_number(user.mobile_number)
|
||||
|
||||
form = ChangeMobileNumberForm(mobile_number=user_mobile_number)
|
||||
if form.mobile_number.data == user_mobile_number and request.method == 'POST':
|
||||
return redirect(url_for(
|
||||
'.manage_users',
|
||||
service_id=service_id
|
||||
))
|
||||
if form.mobile_number.data == user_mobile_number and request.method == "POST":
|
||||
return redirect(url_for(".manage_users", service_id=service_id))
|
||||
if form.validate_on_submit():
|
||||
session['team_member_mobile_change'] = form.mobile_number.data
|
||||
session["team_member_mobile_change"] = form.mobile_number.data
|
||||
|
||||
return redirect(url_for('.confirm_edit_user_mobile_number', user_id=user.id, service_id=service_id))
|
||||
return redirect(
|
||||
url_for(
|
||||
".confirm_edit_user_mobile_number",
|
||||
user_id=user.id,
|
||||
service_id=service_id,
|
||||
)
|
||||
)
|
||||
return render_template(
|
||||
'views/manage-users/edit-user-mobile.html',
|
||||
"views/manage-users/edit-user-mobile.html",
|
||||
user=user,
|
||||
form=form,
|
||||
service_id=service_id
|
||||
service_id=service_id,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number/confirm", methods=['GET', 'POST'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number/confirm",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def confirm_edit_user_mobile_number(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
if 'team_member_mobile_change' in session:
|
||||
new_number = session['team_member_mobile_change']
|
||||
if "team_member_mobile_change" in session:
|
||||
new_number = session["team_member_mobile_change"]
|
||||
else:
|
||||
return redirect(url_for(
|
||||
'.edit_user_mobile_number',
|
||||
service_id=service_id,
|
||||
user_id=user_id
|
||||
))
|
||||
if request.method == 'POST':
|
||||
return redirect(
|
||||
url_for(".edit_user_mobile_number", service_id=service_id, user_id=user_id)
|
||||
)
|
||||
if request.method == "POST":
|
||||
try:
|
||||
user.update(mobile_number=new_number, updated_by=current_user.id)
|
||||
except HTTPError as e:
|
||||
@@ -289,26 +300,26 @@ def confirm_edit_user_mobile_number(service_id, user_id):
|
||||
user_id=user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_mobile_number=user.mobile_number,
|
||||
new_mobile_number=new_number
|
||||
new_mobile_number=new_number,
|
||||
)
|
||||
finally:
|
||||
session.pop('team_member_mobile_change', None)
|
||||
session.pop("team_member_mobile_change", None)
|
||||
|
||||
return redirect(url_for(
|
||||
'.manage_users',
|
||||
service_id=service_id
|
||||
))
|
||||
return redirect(url_for(".manage_users", service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
'views/manage-users/confirm-edit-user-mobile-number.html',
|
||||
"views/manage-users/confirm-edit-user-mobile-number.html",
|
||||
user=user,
|
||||
service_id=service_id,
|
||||
new_mobile_number=new_number
|
||||
new_mobile_number=new_number,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/cancel-invited-user/<uuid:invited_user_id>", methods=['GET'])
|
||||
@user_has_permissions('manage_service')
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/cancel-invited-user/<uuid:invited_user_id>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def cancel_invited_user(service_id, invited_user_id):
|
||||
current_service.cancel_invite(invited_user_id)
|
||||
|
||||
@@ -319,5 +330,5 @@ def cancel_invited_user(service_id, invited_user_id):
|
||||
service_id=service_id,
|
||||
)
|
||||
|
||||
flash(f'Invitation cancelled for {invited_user.email_address}', 'default_with_tick')
|
||||
return redirect(url_for('main.manage_users', service_id=service_id))
|
||||
flash(f"Invitation cancelled for {invited_user.email_address}", "default_with_tick")
|
||||
return redirect(url_for("main.manage_users", service_id=service_id))
|
||||
|
||||
@@ -18,38 +18,49 @@ from app.models.user import User
|
||||
from app.utils.login import log_in_user
|
||||
|
||||
|
||||
@main.route('/new-password/<path:token>', methods=['GET', 'POST'])
|
||||
@main.route("/new-password/<path:token>", methods=["GET", "POST"])
|
||||
def new_password(token):
|
||||
try:
|
||||
token_data = check_token(token, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT'],
|
||||
current_app.config['EMAIL_EXPIRY_SECONDS'])
|
||||
token_data = check_token(
|
||||
token,
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
current_app.config["EMAIL_EXPIRY_SECONDS"],
|
||||
)
|
||||
except SignatureExpired:
|
||||
flash('The link in the email we sent you has expired. Enter your email address to resend.')
|
||||
return redirect(url_for('.forgot_password'))
|
||||
flash(
|
||||
"The link in the email we sent you has expired. Enter your email address to resend."
|
||||
)
|
||||
return redirect(url_for(".forgot_password"))
|
||||
|
||||
email_address = json.loads(token_data)['email']
|
||||
email_address = json.loads(token_data)["email"]
|
||||
user = User.from_email_address(email_address)
|
||||
if user.password_changed_more_recently_than(json.loads(token_data)['created_at']):
|
||||
flash('The link in the email has already been used')
|
||||
return redirect(url_for('main.index'))
|
||||
if user.password_changed_more_recently_than(json.loads(token_data)["created_at"]):
|
||||
flash("The link in the email has already been used")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
if request.method == 'GET':
|
||||
if request.method == "GET":
|
||||
user.update_email_access_validated_at()
|
||||
|
||||
form = NewPasswordForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user.reset_failed_login_count()
|
||||
session['user_details'] = {
|
||||
'id': user.id,
|
||||
'email': user.email_address,
|
||||
'password': form.new_password.data}
|
||||
session["user_details"] = {
|
||||
"id": user.id,
|
||||
"email": user.email_address,
|
||||
"password": form.new_password.data,
|
||||
}
|
||||
if user.email_auth:
|
||||
# they've just clicked an email link, so have done an email auth journey anyway. Just log them in.
|
||||
return log_in_user(user.id)
|
||||
else:
|
||||
# send user a 2fa sms code
|
||||
user.send_verify_code()
|
||||
return redirect(url_for('main.two_factor_sms', next=request.args.get('next')))
|
||||
return redirect(
|
||||
url_for("main.two_factor_sms", next=request.args.get("next"))
|
||||
)
|
||||
else:
|
||||
return render_template('views/new-password.html', token=token, form=form, user=user)
|
||||
return render_template(
|
||||
"views/new-password.html", token=token, form=form, user=user
|
||||
)
|
||||
|
||||
@@ -31,135 +31,137 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
|
||||
@user_has_permissions('view_activity', 'send_messages')
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
def view_notification(service_id, notification_id):
|
||||
notification = notification_api_client.get_notification(service_id, str(notification_id))
|
||||
notification['template'].update({'reply_to_text': notification['reply_to_text']})
|
||||
notification = notification_api_client.get_notification(
|
||||
service_id, str(notification_id)
|
||||
)
|
||||
notification["template"].update({"reply_to_text": notification["reply_to_text"]})
|
||||
|
||||
personalisation = get_all_personalisation_from_notification(notification)
|
||||
error_message = None
|
||||
|
||||
template = get_template(
|
||||
notification['template'],
|
||||
notification["template"],
|
||||
current_service,
|
||||
show_recipient=True,
|
||||
redact_missing_personalisation=True,
|
||||
sms_sender=notification['reply_to_text'],
|
||||
email_reply_to=notification['reply_to_text'],
|
||||
sms_sender=notification["reply_to_text"],
|
||||
email_reply_to=notification["reply_to_text"],
|
||||
)
|
||||
template.values = personalisation
|
||||
if notification['job']:
|
||||
job = job_api_client.get_job(service_id, notification['job']['id'])['data']
|
||||
if notification["job"]:
|
||||
job = job_api_client.get_job(service_id, notification["job"]["id"])["data"]
|
||||
else:
|
||||
job = None
|
||||
|
||||
if get_help_argument() or request.args.get('help') == '0':
|
||||
if get_help_argument() or request.args.get("help") == "0":
|
||||
# help=0 is set when you’ve just sent a notification. We
|
||||
# only want to show the back link when you’ve navigated to a
|
||||
# notification, not when you’ve just sent it.
|
||||
back_link = None
|
||||
elif request.args.get('from_job'):
|
||||
elif request.args.get("from_job"):
|
||||
back_link = url_for(
|
||||
'main.view_job',
|
||||
"main.view_job",
|
||||
service_id=current_service.id,
|
||||
job_id=request.args.get('from_job'),
|
||||
job_id=request.args.get("from_job"),
|
||||
)
|
||||
else:
|
||||
back_link = url_for(
|
||||
'main.view_notifications',
|
||||
"main.view_notifications",
|
||||
service_id=current_service.id,
|
||||
message_type=template.template_type,
|
||||
status='sending,delivered,failed',
|
||||
status="sending,delivered,failed",
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/notifications/notification.html',
|
||||
finished=(notification['status'] in (DELIVERED_STATUSES + FAILURE_STATUSES)),
|
||||
notification_status=notification['status'],
|
||||
"views/notifications/notification.html",
|
||||
finished=(notification["status"] in (DELIVERED_STATUSES + FAILURE_STATUSES)),
|
||||
notification_status=notification["status"],
|
||||
message=error_message,
|
||||
uploaded_file_name='Report',
|
||||
uploaded_file_name="Report",
|
||||
template=template,
|
||||
job=job,
|
||||
updates_url=url_for(
|
||||
".view_notification_updates",
|
||||
service_id=service_id,
|
||||
notification_id=notification['id'],
|
||||
status=request.args.get('status'),
|
||||
help=get_help_argument()
|
||||
notification_id=notification["id"],
|
||||
status=request.args.get("status"),
|
||||
help=get_help_argument(),
|
||||
),
|
||||
partials=get_single_notification_partials(notification),
|
||||
created_by=notification.get('created_by'),
|
||||
created_at=notification['created_at'],
|
||||
updated_at=notification['updated_at'],
|
||||
created_by=notification.get("created_by"),
|
||||
created_at=notification["created_at"],
|
||||
updated_at=notification["updated_at"],
|
||||
help=get_help_argument(),
|
||||
notification_id=notification['id'],
|
||||
can_receive_inbound=(current_service.has_permission('inbound_sms')),
|
||||
sent_with_test_key=(
|
||||
notification.get('key_type') == KEY_TYPE_TEST
|
||||
),
|
||||
notification_id=notification["id"],
|
||||
can_receive_inbound=(current_service.has_permission("inbound_sms")),
|
||||
sent_with_test_key=(notification.get("key_type") == KEY_TYPE_TEST),
|
||||
back_link=back_link,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>.json")
|
||||
@user_has_permissions('view_activity', 'send_messages')
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
def view_notification_updates(service_id, notification_id):
|
||||
return jsonify(**get_single_notification_partials(
|
||||
notification_api_client.get_notification(service_id, notification_id)
|
||||
))
|
||||
return jsonify(
|
||||
**get_single_notification_partials(
|
||||
notification_api_client.get_notification(service_id, notification_id)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_single_notification_partials(notification):
|
||||
return {
|
||||
'status': render_template(
|
||||
'partials/notifications/status.html',
|
||||
"status": render_template(
|
||||
"partials/notifications/status.html",
|
||||
notification=notification,
|
||||
sent_with_test_key=(
|
||||
notification.get('key_type') == KEY_TYPE_TEST
|
||||
),
|
||||
sent_with_test_key=(notification.get("key_type") == KEY_TYPE_TEST),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_all_personalisation_from_notification(notification):
|
||||
if notification["template"].get("redact_personalisation"):
|
||||
notification["personalisation"] = {}
|
||||
|
||||
if notification['template'].get('redact_personalisation'):
|
||||
notification['personalisation'] = {}
|
||||
if notification["template"]["template_type"] == "email":
|
||||
notification["personalisation"]["email_address"] = notification["to"]
|
||||
|
||||
if notification['template']['template_type'] == 'email':
|
||||
notification['personalisation']['email_address'] = notification['to']
|
||||
if notification["template"]["template_type"] == "sms":
|
||||
notification["personalisation"]["phone_number"] = notification["to"]
|
||||
|
||||
if notification['template']['template_type'] == 'sms':
|
||||
notification['personalisation']['phone_number'] = notification['to']
|
||||
|
||||
return notification['personalisation']
|
||||
return notification["personalisation"]
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/download-notifications.csv")
|
||||
@user_has_permissions('view_activity')
|
||||
@user_has_permissions("view_activity")
|
||||
def download_notifications_csv(service_id):
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args['status'] = set_status_filters(filter_args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
|
||||
service_data_retention_days = current_service.get_days_of_retention(filter_args.get('message_type')[0])
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
filter_args.get("message_type")[0]
|
||||
)
|
||||
return Response(
|
||||
stream_with_context(
|
||||
generate_notifications_csv(
|
||||
service_id=service_id,
|
||||
job_id=None,
|
||||
status=filter_args.get('status'),
|
||||
page=request.args.get('page', 1),
|
||||
status=filter_args.get("status"),
|
||||
page=request.args.get("page", 1),
|
||||
page_size=10000,
|
||||
format_for_csv=True,
|
||||
template_type=filter_args.get('message_type'),
|
||||
template_type=filter_args.get("message_type"),
|
||||
limit_days=service_data_retention_days,
|
||||
)
|
||||
),
|
||||
mimetype='text/csv',
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
'Content-Disposition': 'inline; filename="{} - {} - {} report.csv"'.format(
|
||||
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
|
||||
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
|
||||
filter_args['message_type'][0],
|
||||
current_service.name)
|
||||
}
|
||||
filter_args["message_type"][0],
|
||||
current_service.name,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -38,156 +38,156 @@ from app.utils.csv import Spreadsheet
|
||||
from app.utils.user import user_has_permissions, user_is_platform_admin
|
||||
|
||||
|
||||
@main.route("/organizations", methods=['GET'])
|
||||
@main.route("/organizations", methods=["GET"])
|
||||
@user_is_platform_admin
|
||||
def organizations():
|
||||
return render_template(
|
||||
'views/organizations/index.html',
|
||||
"views/organizations/index.html",
|
||||
organizations=AllOrganizations(),
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/add", methods=['GET', 'POST'])
|
||||
@main.route("/organizations/add", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def add_organization():
|
||||
form = AdminNewOrganizationForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
return redirect(url_for(
|
||||
'.organization_settings',
|
||||
org_id=Organization.create_from_form(form).id,
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".organization_settings",
|
||||
org_id=Organization.create_from_form(form).id,
|
||||
)
|
||||
)
|
||||
except HTTPError as e:
|
||||
msg = 'Organization name already exists'
|
||||
msg = "Organization name already exists"
|
||||
if e.status_code == 400 and msg in e.message:
|
||||
form.name.errors.append("This organization name is already in use")
|
||||
else:
|
||||
raise e
|
||||
|
||||
return render_template(
|
||||
'views/organizations/add-organization.html',
|
||||
form=form
|
||||
)
|
||||
return render_template("views/organizations/add-organization.html", form=form)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>", methods=['GET'])
|
||||
@main.route("/organizations/<uuid:org_id>", methods=["GET"])
|
||||
@user_has_permissions()
|
||||
def organization_dashboard(org_id):
|
||||
year, current_financial_year = requested_and_current_financial_year(request)
|
||||
services = current_organization.services_and_usage(
|
||||
financial_year=year
|
||||
)['services']
|
||||
services = current_organization.services_and_usage(financial_year=year)["services"]
|
||||
return render_template(
|
||||
'views/organizations/organization/index.html',
|
||||
"views/organizations/organization/index.html",
|
||||
services=services,
|
||||
years=get_tuples_of_financial_years(
|
||||
partial(url_for, '.organization_dashboard', org_id=current_organization.id),
|
||||
partial(url_for, ".organization_dashboard", org_id=current_organization.id),
|
||||
start=current_financial_year - 2,
|
||||
end=current_financial_year,
|
||||
),
|
||||
selected_year=year,
|
||||
search_form=SearchByNameForm() if len(services) > 7 else None,
|
||||
**{
|
||||
f'total_{key}': sum(service[key] for service in services)
|
||||
for key in ('emails_sent', 'sms_cost')
|
||||
f"total_{key}": sum(service[key] for service in services)
|
||||
for key in ("emails_sent", "sms_cost")
|
||||
},
|
||||
download_link=url_for(
|
||||
'.download_organization_usage_report',
|
||||
org_id=org_id,
|
||||
selected_year=year
|
||||
)
|
||||
".download_organization_usage_report", org_id=org_id, selected_year=year
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/download-usage-report.csv", methods=['GET'])
|
||||
@main.route("/organizations/<uuid:org_id>/download-usage-report.csv", methods=["GET"])
|
||||
@user_has_permissions()
|
||||
def download_organization_usage_report(org_id):
|
||||
selected_year = request.args.get('selected_year')
|
||||
selected_year = request.args.get("selected_year")
|
||||
services_usage = current_organization.services_and_usage(
|
||||
financial_year=selected_year
|
||||
)['services']
|
||||
)["services"]
|
||||
|
||||
unit_column_names = OrderedDict([
|
||||
('service_id', 'Service ID'),
|
||||
('service_name', 'Service Name'),
|
||||
('emails_sent', 'Emails sent'),
|
||||
('sms_remainder', 'Free text message allowance remaining'),
|
||||
])
|
||||
unit_column_names = OrderedDict(
|
||||
[
|
||||
("service_id", "Service ID"),
|
||||
("service_name", "Service Name"),
|
||||
("emails_sent", "Emails sent"),
|
||||
("sms_remainder", "Free text message allowance remaining"),
|
||||
]
|
||||
)
|
||||
|
||||
monetary_column_names = OrderedDict([
|
||||
('sms_cost', 'Spent on text messages ($)'),
|
||||
])
|
||||
monetary_column_names = OrderedDict(
|
||||
[
|
||||
("sms_cost", "Spent on text messages ($)"),
|
||||
]
|
||||
)
|
||||
|
||||
org_usage_data = [
|
||||
list(unit_column_names.values()) + list(monetary_column_names.values())
|
||||
] + [
|
||||
[
|
||||
service[attribute] for attribute in unit_column_names.keys()
|
||||
] + [
|
||||
'{:,.2f}'.format(service[attribute]) for attribute in monetary_column_names.keys()
|
||||
[service[attribute] for attribute in unit_column_names.keys()]
|
||||
+ [
|
||||
"{:,.2f}".format(service[attribute])
|
||||
for attribute in monetary_column_names.keys()
|
||||
]
|
||||
for service in services_usage
|
||||
]
|
||||
|
||||
return Spreadsheet.from_rows(org_usage_data).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': (
|
||||
'inline;'
|
||||
'filename="{} organization usage report for year {}'
|
||||
' - generated on {}.csv"'.format(
|
||||
current_organization.name,
|
||||
selected_year,
|
||||
datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
))
|
||||
}
|
||||
return (
|
||||
Spreadsheet.from_rows(org_usage_data).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": (
|
||||
"inline;"
|
||||
'filename="{} organization usage report for year {}'
|
||||
' - generated on {}.csv"'.format(
|
||||
current_organization.name,
|
||||
selected_year,
|
||||
datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
)
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/trial-services", methods=['GET'])
|
||||
@main.route("/organizations/<uuid:org_id>/trial-services", methods=["GET"])
|
||||
@user_is_platform_admin
|
||||
def organization_trial_mode_services(org_id):
|
||||
return render_template(
|
||||
'views/organizations/organization/trial-mode-services.html',
|
||||
"views/organizations/organization/trial-mode-services.html",
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/users", methods=['GET'])
|
||||
@main.route("/organizations/<uuid:org_id>/users", methods=["GET"])
|
||||
@user_has_permissions()
|
||||
def manage_org_users(org_id):
|
||||
return render_template(
|
||||
'views/organizations/organization/users/index.html',
|
||||
"views/organizations/organization/users/index.html",
|
||||
users=current_organization.team_members,
|
||||
show_search_box=(len(current_organization.team_members) > 7),
|
||||
form=SearchUsersForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/users/invite", methods=['GET', 'POST'])
|
||||
@main.route("/organizations/<uuid:org_id>/users/invite", methods=["GET", "POST"])
|
||||
@user_has_permissions()
|
||||
def invite_org_user(org_id):
|
||||
form = InviteOrgUserForm(
|
||||
inviter_email_address=current_user.email_address
|
||||
)
|
||||
form = InviteOrgUserForm(inviter_email_address=current_user.email_address)
|
||||
if form.validate_on_submit():
|
||||
email_address = form.email_address.data
|
||||
invited_org_user = InvitedOrgUser.create(
|
||||
current_user.id,
|
||||
org_id,
|
||||
email_address
|
||||
)
|
||||
invited_org_user = InvitedOrgUser.create(current_user.id, org_id, email_address)
|
||||
|
||||
flash('Invite sent to {}'.format(invited_org_user.email_address), 'default_with_tick')
|
||||
return redirect(url_for('.manage_org_users', org_id=org_id))
|
||||
flash(
|
||||
"Invite sent to {}".format(invited_org_user.email_address),
|
||||
"default_with_tick",
|
||||
)
|
||||
return redirect(url_for(".manage_org_users", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/users/invite-org-user.html',
|
||||
form=form
|
||||
"views/organizations/organization/users/invite-org-user.html", form=form
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/users/<uuid:user_id>", methods=['GET'])
|
||||
@main.route("/organizations/<uuid:org_id>/users/<uuid:user_id>", methods=["GET"])
|
||||
@user_has_permissions()
|
||||
def edit_organization_user(org_id, user_id):
|
||||
# The only action that can be done to an org user is to remove them from the org.
|
||||
@@ -195,69 +195,77 @@ def edit_organization_user(org_id, user_id):
|
||||
# query string, but it uses the template for all org team members in order to avoid
|
||||
# having a page containing a single link.
|
||||
return render_template(
|
||||
'views/organizations/organization/users/index.html',
|
||||
"views/organizations/organization/users/index.html",
|
||||
users=current_organization.team_members,
|
||||
show_search_box=(len(current_organization.team_members) > 7),
|
||||
form=SearchUsersForm(),
|
||||
user_to_remove=User.from_id(user_id)
|
||||
user_to_remove=User.from_id(user_id),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/users/<uuid:user_id>/delete", methods=['POST'])
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/users/<uuid:user_id>/delete", methods=["POST"]
|
||||
)
|
||||
@user_has_permissions()
|
||||
def remove_user_from_organization(org_id, user_id):
|
||||
organizations_client.remove_user_from_organization(org_id, user_id)
|
||||
|
||||
return redirect(url_for('.show_accounts_or_dashboard'))
|
||||
return redirect(url_for(".show_accounts_or_dashboard"))
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/cancel-invited-user/<uuid:invited_user_id>", methods=['GET'])
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/cancel-invited-user/<uuid:invited_user_id>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions()
|
||||
def cancel_invited_org_user(org_id, invited_user_id):
|
||||
org_invite_api_client.cancel_invited_user(org_id=org_id, invited_user_id=invited_user_id)
|
||||
org_invite_api_client.cancel_invited_user(
|
||||
org_id=org_id, invited_user_id=invited_user_id
|
||||
)
|
||||
|
||||
invited_org_user = InvitedOrgUser.by_id_and_org_id(org_id, invited_user_id)
|
||||
|
||||
flash(f'Invitation cancelled for {invited_org_user.email_address}', 'default_with_tick')
|
||||
return redirect(url_for('main.manage_org_users', org_id=org_id))
|
||||
flash(
|
||||
f"Invitation cancelled for {invited_org_user.email_address}",
|
||||
"default_with_tick",
|
||||
)
|
||||
return redirect(url_for("main.manage_org_users", org_id=org_id))
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/", methods=['GET'])
|
||||
@main.route("/organizations/<uuid:org_id>/settings/", methods=["GET"])
|
||||
@user_is_platform_admin
|
||||
def organization_settings(org_id):
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/index.html',
|
||||
"views/organizations/organization/settings/index.html",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/edit-name", methods=['GET', 'POST'])
|
||||
@main.route("/organizations/<uuid:org_id>/settings/edit-name", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_organization_name(org_id):
|
||||
form = RenameOrganizationForm(name=current_organization.name)
|
||||
|
||||
if form.validate_on_submit():
|
||||
|
||||
try:
|
||||
current_organization.update(name=form.name.data)
|
||||
except HTTPError as http_error:
|
||||
error_msg = 'Organization name already exists'
|
||||
error_msg = "Organization name already exists"
|
||||
if http_error.status_code == 400 and error_msg in http_error.message:
|
||||
form.name.errors.append('This organization name is already in use')
|
||||
form.name.errors.append("This organization name is already in use")
|
||||
else:
|
||||
raise http_error
|
||||
else:
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/edit-name.html',
|
||||
"views/organizations/organization/settings/edit-name.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/edit-type", methods=['GET', 'POST'])
|
||||
@main.route("/organizations/<uuid:org_id>/settings/edit-type", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_organization_type(org_id):
|
||||
|
||||
form = OrganizationOrganizationTypeForm(
|
||||
organization_type=current_organization.organization_type
|
||||
)
|
||||
@@ -267,18 +275,19 @@ def edit_organization_type(org_id):
|
||||
organization_type=form.organization_type.data,
|
||||
delete_services_cache=True,
|
||||
)
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/edit-type.html',
|
||||
"views/organizations/organization/settings/edit-type.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/set-email-branding", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/set-email-branding", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_email_branding(org_id):
|
||||
|
||||
email_branding = email_branding_client.get_all_email_branding()
|
||||
|
||||
form = AdminSetEmailBrandingForm(
|
||||
@@ -287,24 +296,28 @@ def edit_organization_email_branding(org_id):
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for(
|
||||
'.organization_preview_email_branding',
|
||||
org_id=org_id,
|
||||
branding_style=form.branding_style.data,
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".organization_preview_email_branding",
|
||||
org_id=org_id,
|
||||
branding_style=form.branding_style.data,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/set-email-branding.html',
|
||||
"views/organizations/organization/settings/set-email-branding.html",
|
||||
form=form,
|
||||
search_form=SearchByNameForm()
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/preview-email-branding", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/preview-email-branding",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def organization_preview_email_branding(org_id):
|
||||
|
||||
branding_style = request.args.get('branding_style', None)
|
||||
branding_style = request.args.get("branding_style", None)
|
||||
|
||||
form = AdminPreviewBrandingForm(branding_style=branding_style)
|
||||
|
||||
@@ -313,94 +326,97 @@ def organization_preview_email_branding(org_id):
|
||||
email_branding_id=form.branding_style.data,
|
||||
delete_services_cache=True,
|
||||
)
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/preview-email-branding.html',
|
||||
"views/organizations/organization/settings/preview-email-branding.html",
|
||||
form=form,
|
||||
action=url_for('main.organization_preview_email_branding', org_id=org_id),
|
||||
action=url_for("main.organization_preview_email_branding", org_id=org_id),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/edit-organization-domains", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/edit-organization-domains",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_domains(org_id):
|
||||
|
||||
form = AdminOrganizationDomainsForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
organizations_client.update_organization(
|
||||
org_id,
|
||||
domains=list(OrderedDict.fromkeys(
|
||||
domain.lower()
|
||||
for domain in filter(None, form.domains.data)
|
||||
)),
|
||||
domains=list(
|
||||
OrderedDict.fromkeys(
|
||||
domain.lower() for domain in filter(None, form.domains.data)
|
||||
)
|
||||
),
|
||||
)
|
||||
except HTTPError as e:
|
||||
error_message = "Domain already exists"
|
||||
if e.status_code == 400 and error_message in e.message:
|
||||
flash("This domain is already in use", "error")
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/edit-domains.html',
|
||||
"views/organizations/organization/settings/edit-domains.html",
|
||||
form=form,
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
form.populate(current_organization.domains)
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/edit-domains.html',
|
||||
"views/organizations/organization/settings/edit-domains.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/edit-go-live-notes", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/edit-go-live-notes", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_go_live_notes(org_id):
|
||||
|
||||
form = AdminOrganizationGoLiveNotesForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
organizations_client.update_organization(
|
||||
org_id,
|
||||
request_to_go_live_notes=form.request_to_go_live_notes.data
|
||||
org_id, request_to_go_live_notes=form.request_to_go_live_notes.data
|
||||
)
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
org = organizations_client.get_organization(org_id)
|
||||
form.request_to_go_live_notes.data = org['request_to_go_live_notes']
|
||||
form.request_to_go_live_notes.data = org["request_to_go_live_notes"]
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/edit-go-live-notes.html',
|
||||
"views/organizations/organization/settings/edit-go-live-notes.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/notes", methods=['GET', 'POST'])
|
||||
@main.route("/organizations/<uuid:org_id>/settings/notes", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_organization_notes(org_id):
|
||||
form = AdminNotesForm(notes=current_organization.notes)
|
||||
|
||||
if form.validate_on_submit():
|
||||
|
||||
if form.notes.data == current_organization.notes:
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
current_organization.update(
|
||||
notes=form.notes.data
|
||||
)
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
current_organization.update(notes=form.notes.data)
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/edit-organization-notes.html',
|
||||
"views/organizations/organization/settings/edit-organization-notes.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/edit-billing-details", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/edit-billing-details",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_billing_details(org_id):
|
||||
form = AdminBillingDetailsForm(
|
||||
@@ -419,10 +435,10 @@ def edit_organization_billing_details(org_id):
|
||||
purchase_order_number=form.purchase_order_number.data,
|
||||
notes=form.notes.data,
|
||||
)
|
||||
return redirect(url_for('.organization_settings', org_id=org_id))
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
'views/organizations/organization/settings/edit-organization-billing-details.html',
|
||||
"views/organizations/organization/settings/edit-organization-billing-details.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
@@ -430,6 +446,4 @@ def edit_organization_billing_details(org_id):
|
||||
@main.route("/organizations/<uuid:org_id>/billing")
|
||||
@user_is_platform_admin
|
||||
def organization_billing(org_id):
|
||||
return render_template(
|
||||
'views/organizations/organization/billing.html'
|
||||
)
|
||||
return render_template("views/organizations/organization/billing.html")
|
||||
|
||||
@@ -16,27 +16,23 @@ def performance():
|
||||
start_date=(datetime.now(pytz.utc) - timedelta(days=7)).date(),
|
||||
end_date=datetime.now(pytz.utc).date(),
|
||||
)
|
||||
stats['organizations_using_notify'] = sorted(
|
||||
stats["organizations_using_notify"] = sorted(
|
||||
[
|
||||
{
|
||||
'organization_name': organization_name or 'No organization',
|
||||
'count_of_live_services': len(list(group)),
|
||||
"organization_name": organization_name or "No organization",
|
||||
"count_of_live_services": len(list(group)),
|
||||
}
|
||||
for organization_name, group in groupby(
|
||||
stats['services_using_notify'],
|
||||
itemgetter('organization_name'),
|
||||
stats["services_using_notify"],
|
||||
itemgetter("organization_name"),
|
||||
)
|
||||
],
|
||||
key=itemgetter('organization_name'),
|
||||
key=itemgetter("organization_name"),
|
||||
)
|
||||
stats['average_percentage_under_10_seconds'] = mean([
|
||||
row['percentage_under_10_seconds']
|
||||
for row in stats['processing_time']
|
||||
] or [0])
|
||||
stats['count_of_live_services_and_organizations'] = (
|
||||
status_api_client.get_count_of_live_services_and_organizations()
|
||||
)
|
||||
return render_template(
|
||||
'views/performance.html',
|
||||
**stats
|
||||
stats["average_percentage_under_10_seconds"] = mean(
|
||||
[row["percentage_under_10_seconds"] for row in stats["processing_time"]] or [0]
|
||||
)
|
||||
stats[
|
||||
"count_of_live_services_and_organizations"
|
||||
] = status_api_client.get_count_of_live_services_and_organizations()
|
||||
return render_template("views/performance.html", **stats)
|
||||
|
||||
@@ -44,29 +44,29 @@ ZERO_FAILURE_THRESHOLD = 0
|
||||
@user_is_platform_admin
|
||||
def platform_admin_splash_page():
|
||||
return render_template(
|
||||
'views/platform-admin/splash-page.html',
|
||||
"views/platform-admin/splash-page.html",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/summary")
|
||||
@user_is_platform_admin
|
||||
def platform_admin():
|
||||
form = DateFilterForm(request.args, meta={'csrf': False})
|
||||
form = DateFilterForm(request.args, meta={"csrf": False})
|
||||
api_args = {}
|
||||
|
||||
form.validate()
|
||||
|
||||
if form.start_date.data:
|
||||
api_args['start_date'] = form.start_date.data
|
||||
api_args['end_date'] = form.end_date.data or datetime.utcnow().date()
|
||||
api_args["start_date"] = form.start_date.data
|
||||
api_args["end_date"] = form.end_date.data or datetime.utcnow().date()
|
||||
|
||||
platform_stats = platform_stats_api_client.get_aggregate_platform_stats(api_args)
|
||||
number_of_complaints = complaint_api_client.get_complaint_count(api_args)
|
||||
|
||||
return render_template(
|
||||
'views/platform-admin/index.html',
|
||||
"views/platform-admin/index.html",
|
||||
form=form,
|
||||
global_stats=make_columns(platform_stats, number_of_complaints)
|
||||
global_stats=make_columns(platform_stats, number_of_complaints),
|
||||
)
|
||||
|
||||
|
||||
@@ -77,20 +77,18 @@ def is_over_threshold(number, total, threshold):
|
||||
|
||||
def get_status_box_data(stats, key, label, threshold=FAILURE_THRESHOLD):
|
||||
return {
|
||||
'number': "{:,}".format(stats['failures'][key]),
|
||||
'label': label,
|
||||
'failing': is_over_threshold(
|
||||
stats['failures'][key],
|
||||
stats['total'],
|
||||
threshold
|
||||
),
|
||||
'percentage': get_formatted_percentage(stats['failures'][key], stats['total'])
|
||||
"number": "{:,}".format(stats["failures"][key]),
|
||||
"label": label,
|
||||
"failing": is_over_threshold(stats["failures"][key], stats["total"], threshold),
|
||||
"percentage": get_formatted_percentage(stats["failures"][key], stats["total"]),
|
||||
}
|
||||
|
||||
|
||||
def get_tech_failure_status_box_data(stats):
|
||||
stats = get_status_box_data(stats, 'technical-failure', 'technical failures', ZERO_FAILURE_THRESHOLD)
|
||||
stats.pop('percentage')
|
||||
stats = get_status_box_data(
|
||||
stats, "technical-failure", "technical failures", ZERO_FAILURE_THRESHOLD
|
||||
)
|
||||
stats.pop("percentage")
|
||||
return stats
|
||||
|
||||
|
||||
@@ -98,83 +96,99 @@ def make_columns(global_stats, complaints_number):
|
||||
return [
|
||||
# email
|
||||
{
|
||||
'black_box': {
|
||||
'number': global_stats['email']['total'],
|
||||
'notification_type': 'email'
|
||||
"black_box": {
|
||||
"number": global_stats["email"]["total"],
|
||||
"notification_type": "email",
|
||||
},
|
||||
'other_data': [
|
||||
get_tech_failure_status_box_data(global_stats['email']),
|
||||
get_status_box_data(global_stats['email'], 'permanent-failure', 'permanent failures'),
|
||||
get_status_box_data(global_stats['email'], 'temporary-failure', 'temporary failures'),
|
||||
"other_data": [
|
||||
get_tech_failure_status_box_data(global_stats["email"]),
|
||||
get_status_box_data(
|
||||
global_stats["email"], "permanent-failure", "permanent failures"
|
||||
),
|
||||
get_status_box_data(
|
||||
global_stats["email"], "temporary-failure", "temporary failures"
|
||||
),
|
||||
{
|
||||
'number': complaints_number,
|
||||
'label': 'complaints',
|
||||
'failing': is_over_threshold(complaints_number,
|
||||
global_stats['email']['total'], COMPLAINT_THRESHOLD),
|
||||
'percentage': get_formatted_percentage_two_dp(complaints_number, global_stats['email']['total']),
|
||||
'url': url_for('main.platform_admin_list_complaints')
|
||||
}
|
||||
"number": complaints_number,
|
||||
"label": "complaints",
|
||||
"failing": is_over_threshold(
|
||||
complaints_number,
|
||||
global_stats["email"]["total"],
|
||||
COMPLAINT_THRESHOLD,
|
||||
),
|
||||
"percentage": get_formatted_percentage_two_dp(
|
||||
complaints_number, global_stats["email"]["total"]
|
||||
),
|
||||
"url": url_for("main.platform_admin_list_complaints"),
|
||||
},
|
||||
],
|
||||
'test_data': {
|
||||
'number': global_stats['email']['test-key'],
|
||||
'label': 'test emails'
|
||||
}
|
||||
"test_data": {
|
||||
"number": global_stats["email"]["test-key"],
|
||||
"label": "test emails",
|
||||
},
|
||||
},
|
||||
# sms
|
||||
{
|
||||
'black_box': {
|
||||
'number': global_stats['sms']['total'],
|
||||
'notification_type': 'sms'
|
||||
"black_box": {
|
||||
"number": global_stats["sms"]["total"],
|
||||
"notification_type": "sms",
|
||||
},
|
||||
'other_data': [
|
||||
get_tech_failure_status_box_data(global_stats['sms']),
|
||||
get_status_box_data(global_stats['sms'], 'permanent-failure', 'permanent failures'),
|
||||
get_status_box_data(global_stats['sms'], 'temporary-failure', 'temporary failures')
|
||||
"other_data": [
|
||||
get_tech_failure_status_box_data(global_stats["sms"]),
|
||||
get_status_box_data(
|
||||
global_stats["sms"], "permanent-failure", "permanent failures"
|
||||
),
|
||||
get_status_box_data(
|
||||
global_stats["sms"], "temporary-failure", "temporary failures"
|
||||
),
|
||||
],
|
||||
'test_data': {
|
||||
'number': global_stats['sms']['test-key'],
|
||||
'label': 'test text messages'
|
||||
}
|
||||
"test_data": {
|
||||
"number": global_stats["sms"]["test-key"],
|
||||
"label": "test text messages",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@main.route("/platform-admin/live-services", endpoint='live_services')
|
||||
@main.route("/platform-admin/trial-services", endpoint='trial_services')
|
||||
@main.route("/platform-admin/live-services", endpoint="live_services")
|
||||
@main.route("/platform-admin/trial-services", endpoint="trial_services")
|
||||
@user_is_platform_admin
|
||||
def platform_admin_services():
|
||||
form = DateFilterForm(request.args)
|
||||
if all((
|
||||
request.args.get('include_from_test_key') is None,
|
||||
request.args.get('start_date') is None,
|
||||
request.args.get('end_date') is None,
|
||||
)):
|
||||
if all(
|
||||
(
|
||||
request.args.get("include_from_test_key") is None,
|
||||
request.args.get("start_date") is None,
|
||||
request.args.get("end_date") is None,
|
||||
)
|
||||
):
|
||||
# Default to True if the user hasn’t done any filtering,
|
||||
# otherwise respect their choice
|
||||
form.include_from_test_key.data = True
|
||||
|
||||
include_from_test_key = form.include_from_test_key.data
|
||||
api_args = {'detailed': True,
|
||||
'only_active': False, # specifically DO get inactive services
|
||||
'include_from_test_key': include_from_test_key,
|
||||
}
|
||||
api_args = {
|
||||
"detailed": True,
|
||||
"only_active": False, # specifically DO get inactive services
|
||||
"include_from_test_key": include_from_test_key,
|
||||
}
|
||||
|
||||
if form.start_date.data:
|
||||
api_args['start_date'] = form.start_date.data
|
||||
api_args['end_date'] = form.end_date.data or datetime.utcnow().date()
|
||||
api_args["start_date"] = form.start_date.data
|
||||
api_args["end_date"] = form.end_date.data or datetime.utcnow().date()
|
||||
|
||||
services = filter_and_sort_services(
|
||||
service_api_client.get_services(api_args)['data'],
|
||||
trial_mode_services=request.endpoint == 'main.trial_services',
|
||||
service_api_client.get_services(api_args)["data"],
|
||||
trial_mode_services=request.endpoint == "main.trial_services",
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/platform-admin/services.html',
|
||||
"views/platform-admin/services.html",
|
||||
include_from_test_key=include_from_test_key,
|
||||
form=form,
|
||||
services=list(format_stats_by_service(services)),
|
||||
page_title='{} services'.format(
|
||||
'Trial mode' if request.endpoint == 'main.trial_services' else 'Live'
|
||||
page_title="{} services".format(
|
||||
"Trial mode" if request.endpoint == "main.trial_services" else "Live"
|
||||
),
|
||||
global_stats=create_global_stats(services),
|
||||
)
|
||||
@@ -183,9 +197,7 @@ def platform_admin_services():
|
||||
@main.route("/platform-admin/reports")
|
||||
@user_is_platform_admin
|
||||
def platform_admin_reports():
|
||||
return render_template(
|
||||
'views/platform-admin/reports.html'
|
||||
)
|
||||
return render_template("views/platform-admin/reports.html")
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/live-services.csv")
|
||||
@@ -193,41 +205,51 @@ def platform_admin_reports():
|
||||
def live_services_csv():
|
||||
results = service_api_client.get_live_services_data()["data"]
|
||||
|
||||
column_names = OrderedDict([
|
||||
('service_id', 'Service ID'),
|
||||
('organization_name', 'Organization'),
|
||||
('organization_type', 'Organization type'),
|
||||
('service_name', 'Service name'),
|
||||
('consent_to_research', 'Consent to research'),
|
||||
('contact_name', 'Main contact'),
|
||||
('contact_email', 'Contact email'),
|
||||
('contact_mobile', 'Contact mobile'),
|
||||
('live_date', 'Live date'),
|
||||
('sms_volume_intent', 'SMS volume intent'),
|
||||
('email_volume_intent', 'Email volume intent'),
|
||||
('sms_totals', 'SMS sent this year'),
|
||||
('email_totals', 'Emails sent this year'),
|
||||
('free_sms_fragment_limit', 'Free sms allowance'),
|
||||
])
|
||||
column_names = OrderedDict(
|
||||
[
|
||||
("service_id", "Service ID"),
|
||||
("organization_name", "Organization"),
|
||||
("organization_type", "Organization type"),
|
||||
("service_name", "Service name"),
|
||||
("consent_to_research", "Consent to research"),
|
||||
("contact_name", "Main contact"),
|
||||
("contact_email", "Contact email"),
|
||||
("contact_mobile", "Contact mobile"),
|
||||
("live_date", "Live date"),
|
||||
("sms_volume_intent", "SMS volume intent"),
|
||||
("email_volume_intent", "Email volume intent"),
|
||||
("sms_totals", "SMS sent this year"),
|
||||
("email_totals", "Emails sent this year"),
|
||||
("free_sms_fragment_limit", "Free sms allowance"),
|
||||
]
|
||||
)
|
||||
|
||||
# initialise with header row
|
||||
live_services_data = [[x for x in column_names.values()]]
|
||||
|
||||
for row in results:
|
||||
if row['live_date']:
|
||||
row['live_date'] = datetime.strptime(row["live_date"], '%a, %d %b %Y %X %Z').strftime("%d-%m-%Y")
|
||||
if row["live_date"]:
|
||||
row["live_date"] = datetime.strptime(
|
||||
row["live_date"], "%a, %d %b %Y %X %Z"
|
||||
).strftime("%d-%m-%Y")
|
||||
|
||||
live_services_data.append([row[api_key] for api_key in column_names.keys()])
|
||||
|
||||
return Spreadsheet.from_rows(live_services_data).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'inline; filename="{} live services report.csv"'.format(
|
||||
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
|
||||
)
|
||||
}
|
||||
return (
|
||||
Spreadsheet.from_rows(live_services_data).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": 'inline; filename="{} live services report.csv"'.format(
|
||||
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/notifications-sent-by-service", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/platform-admin/reports/notifications-sent-by-service", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def notifications_sent_by_service():
|
||||
form = RequiredDateFilterForm()
|
||||
@@ -237,21 +259,39 @@ def notifications_sent_by_service():
|
||||
end_date = form.end_date.data
|
||||
|
||||
headers = [
|
||||
'date_created', 'service_id', 'service_name', 'notification_type', 'count_sending', 'count_delivered',
|
||||
'count_technical_failure', 'count_temporary_failure', 'count_permanent_failure', 'count_sent'
|
||||
"date_created",
|
||||
"service_id",
|
||||
"service_name",
|
||||
"notification_type",
|
||||
"count_sending",
|
||||
"count_delivered",
|
||||
"count_technical_failure",
|
||||
"count_temporary_failure",
|
||||
"count_permanent_failure",
|
||||
"count_sent",
|
||||
]
|
||||
result = notification_api_client.get_notification_status_by_service(start_date, end_date)
|
||||
result = notification_api_client.get_notification_status_by_service(
|
||||
start_date, end_date
|
||||
)
|
||||
content_disposition = (
|
||||
'attachment; filename="{} to {} notification status '
|
||||
'per service report.csv"'.format(start_date, end_date)
|
||||
)
|
||||
return (
|
||||
Spreadsheet.from_rows([headers] + result).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": content_disposition,
|
||||
},
|
||||
)
|
||||
|
||||
return Spreadsheet.from_rows([headers] + result).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="{} to {} notification status per service report.csv"'.format(
|
||||
start_date, end_date)
|
||||
}
|
||||
|
||||
return render_template('views/platform-admin/notifications_by_service.html', form=form)
|
||||
return render_template(
|
||||
"views/platform-admin/notifications_by_service.html", form=form
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/usage-for-all-services", methods=['GET', 'POST'])
|
||||
@main.route("/platform-admin/reports/usage-for-all-services", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def get_billing_report():
|
||||
form = BillingReportDateFilterForm()
|
||||
@@ -260,46 +300,71 @@ def get_billing_report():
|
||||
start_date = form.start_date.data
|
||||
end_date = form.end_date.data
|
||||
headers = [
|
||||
"organization_id", "organization_name", "service_id", "service_name",
|
||||
"sms_cost", "sms_chargeable_units",
|
||||
"purchase_order_number", "contact_names", "contact_email_addresses", "billing_reference"
|
||||
"organization_id",
|
||||
"organization_name",
|
||||
"service_id",
|
||||
"service_name",
|
||||
"sms_cost",
|
||||
"sms_chargeable_units",
|
||||
"purchase_order_number",
|
||||
"contact_names",
|
||||
"contact_email_addresses",
|
||||
"billing_reference",
|
||||
]
|
||||
try:
|
||||
result = billing_api_client.get_data_for_billing_report(start_date, end_date)
|
||||
result = billing_api_client.get_data_for_billing_report(
|
||||
start_date, end_date
|
||||
)
|
||||
except HTTPError as e:
|
||||
message = 'Date must be in a single financial year.'
|
||||
message = "Date must be in a single financial year."
|
||||
if e.status_code == 400 and e.message == message:
|
||||
flash(message)
|
||||
return render_template('views/platform-admin/get-billing-report.html', form=form)
|
||||
return render_template(
|
||||
"views/platform-admin/get-billing-report.html", form=form
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
rows = [
|
||||
[
|
||||
r["organization_id"], r["organization_name"], r["service_id"], r["service_name"],
|
||||
r["sms_cost"], r["sms_chargeable_units"],
|
||||
r.get("purchase_order_number"), r.get("contact_names"),
|
||||
r.get("contact_email_addresses"), r.get("billing_reference")
|
||||
r["organization_id"],
|
||||
r["organization_name"],
|
||||
r["service_id"],
|
||||
r["service_name"],
|
||||
r["sms_cost"],
|
||||
r["sms_chargeable_units"],
|
||||
r.get("purchase_order_number"),
|
||||
r.get("contact_names"),
|
||||
r.get("contact_email_addresses"),
|
||||
r.get("billing_reference"),
|
||||
]
|
||||
for r in result
|
||||
]
|
||||
if rows:
|
||||
return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="Billing Report from {} to {}.csv"'.format(
|
||||
start_date, end_date
|
||||
)
|
||||
}
|
||||
return (
|
||||
Spreadsheet.from_rows([headers] + rows).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": 'attachment; filename="Billing Report from {} to {}.csv"'.format(
|
||||
start_date, end_date
|
||||
),
|
||||
},
|
||||
)
|
||||
else:
|
||||
flash('No results for dates')
|
||||
return render_template('views/platform-admin/get-billing-report.html', form=form)
|
||||
flash("No results for dates")
|
||||
return render_template("views/platform-admin/get-billing-report.html", form=form)
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/get-users-report", methods=['GET', 'POST'])
|
||||
@main.route("/platform-admin/reports/get-users-report", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def get_users_report():
|
||||
headers = [
|
||||
"name", "services", "platform admin", "permissions", "password changed at",
|
||||
"state"
|
||||
"name",
|
||||
"services",
|
||||
"platform admin",
|
||||
"permissions",
|
||||
"password changed at",
|
||||
"state",
|
||||
]
|
||||
try:
|
||||
result = user_api_client.get_all_users()
|
||||
@@ -311,16 +376,20 @@ def get_users_report():
|
||||
for r in result:
|
||||
rows.append(_get_user_row(r))
|
||||
if rows:
|
||||
return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': f'attachment; filename="User Report {datetime.utcnow()}.csv"'
|
||||
}
|
||||
return (
|
||||
Spreadsheet.from_rows([headers] + rows).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": f'attachment; filename="User Report {datetime.utcnow()}.csv"',
|
||||
},
|
||||
)
|
||||
else:
|
||||
flash('No results')
|
||||
return render_template('views/platform-admin/get-users-report.html')
|
||||
flash("No results")
|
||||
return render_template("views/platform-admin/get-users-report.html")
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/volumes-by-service", methods=['GET', 'POST'])
|
||||
@main.route("/platform-admin/reports/volumes-by-service", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def get_volumes_by_service():
|
||||
form = BillingReportDateFilterForm()
|
||||
@@ -329,31 +398,51 @@ def get_volumes_by_service():
|
||||
start_date = form.start_date.data
|
||||
end_date = form.end_date.data
|
||||
headers = [
|
||||
"organization id", "organization name", "service id", "service name",
|
||||
"free allowance", "sms notifications", "sms chargeable units", "email totals",
|
||||
"organization id",
|
||||
"organization name",
|
||||
"service id",
|
||||
"service name",
|
||||
"free allowance",
|
||||
"sms notifications",
|
||||
"sms chargeable units",
|
||||
"email totals",
|
||||
]
|
||||
result = billing_api_client.get_data_for_volumes_by_service_report(start_date, end_date)
|
||||
result = billing_api_client.get_data_for_volumes_by_service_report(
|
||||
start_date, end_date
|
||||
)
|
||||
|
||||
rows = [
|
||||
[
|
||||
r["organization_id"], r["organization_name"], r["service_id"], r["service_name"],
|
||||
r["free_allowance"], r["sms_notifications"], r["sms_chargeable_units"], r["email_totals"],
|
||||
r["organization_id"],
|
||||
r["organization_name"],
|
||||
r["service_id"],
|
||||
r["service_name"],
|
||||
r["free_allowance"],
|
||||
r["sms_notifications"],
|
||||
r["sms_chargeable_units"],
|
||||
r["email_totals"],
|
||||
]
|
||||
for r in result
|
||||
]
|
||||
if rows:
|
||||
return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="Volumes by service report from {} to {}.csv"'.format(
|
||||
start_date, end_date
|
||||
)
|
||||
}
|
||||
return (
|
||||
Spreadsheet.from_rows([headers] + rows).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": 'attachment; filename="Volumes by service report from {} to {}.csv"'.format(
|
||||
start_date, end_date
|
||||
),
|
||||
},
|
||||
)
|
||||
else:
|
||||
flash('No results for dates')
|
||||
return render_template('views/platform-admin/volumes-by-service-report.html', form=form)
|
||||
flash("No results for dates")
|
||||
return render_template(
|
||||
"views/platform-admin/volumes-by-service-report.html", form=form
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/daily-volumes-report", methods=['GET', 'POST'])
|
||||
@main.route("/platform-admin/reports/daily-volumes-report", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def get_daily_volumes():
|
||||
form = BillingReportDateFilterForm()
|
||||
@@ -362,31 +451,45 @@ def get_daily_volumes():
|
||||
start_date = form.start_date.data
|
||||
end_date = form.end_date.data
|
||||
headers = [
|
||||
"day", "sms totals", "sms fragment totals", "sms chargeable units",
|
||||
"day",
|
||||
"sms totals",
|
||||
"sms fragment totals",
|
||||
"sms chargeable units",
|
||||
"email totals",
|
||||
]
|
||||
result = billing_api_client.get_data_for_daily_volumes_report(start_date, end_date)
|
||||
result = billing_api_client.get_data_for_daily_volumes_report(
|
||||
start_date, end_date
|
||||
)
|
||||
|
||||
rows = [
|
||||
[
|
||||
r["day"], r["sms_totals"], r["sms_fragment_totals"], r["sms_chargeable_units"],
|
||||
r["day"],
|
||||
r["sms_totals"],
|
||||
r["sms_fragment_totals"],
|
||||
r["sms_chargeable_units"],
|
||||
r["email_totals"],
|
||||
]
|
||||
for r in result
|
||||
]
|
||||
if rows:
|
||||
return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="Daily volumes report from {} to {}.csv"'.format(
|
||||
start_date, end_date
|
||||
)
|
||||
}
|
||||
return (
|
||||
Spreadsheet.from_rows([headers] + rows).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": 'attachment; filename="Daily volumes report from {} to {}.csv"'.format(
|
||||
start_date, end_date
|
||||
),
|
||||
},
|
||||
)
|
||||
else:
|
||||
flash('No results for dates')
|
||||
return render_template('views/platform-admin/daily-volumes-report.html', form=form)
|
||||
flash("No results for dates")
|
||||
return render_template("views/platform-admin/daily-volumes-report.html", form=form)
|
||||
|
||||
|
||||
@main.route("/platform-admin/reports/daily-sms-provider-volumes-report", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/platform-admin/reports/daily-sms-provider-volumes-report", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def get_daily_sms_provider_volumes():
|
||||
form = BillingReportDateFilterForm()
|
||||
@@ -402,7 +505,9 @@ def get_daily_sms_provider_volumes():
|
||||
"sms chargeable units",
|
||||
"sms cost",
|
||||
]
|
||||
result = billing_api_client.get_data_for_daily_sms_provider_volumes_report(start_date, end_date)
|
||||
result = billing_api_client.get_data_for_daily_sms_provider_volumes_report(
|
||||
start_date, end_date
|
||||
)
|
||||
|
||||
rows = [
|
||||
[
|
||||
@@ -411,19 +516,25 @@ def get_daily_sms_provider_volumes():
|
||||
r["sms_totals"],
|
||||
r["sms_fragment_totals"],
|
||||
r["sms_chargeable_units"],
|
||||
r["sms_cost"]
|
||||
r["sms_cost"],
|
||||
]
|
||||
for r in result
|
||||
]
|
||||
content_disp = f'attachment; filename="Daily SMS provider volumes report from {start_date} to {end_date}.csv"'
|
||||
if rows:
|
||||
return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition':
|
||||
f'attachment; filename="Daily SMS provider volumes report from {start_date} to {end_date}.csv"'
|
||||
}
|
||||
return (
|
||||
Spreadsheet.from_rows([headers] + rows).as_csv_data,
|
||||
200,
|
||||
{
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": content_disp,
|
||||
},
|
||||
)
|
||||
else:
|
||||
flash('No results for dates')
|
||||
return render_template('views/platform-admin/daily-sms-provider-volumes-report.html', form=form)
|
||||
flash("No results for dates")
|
||||
return render_template(
|
||||
"views/platform-admin/daily-sms-provider-volumes-report.html", form=form
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/complaints")
|
||||
@@ -431,62 +542,83 @@ def get_daily_sms_provider_volumes():
|
||||
def platform_admin_list_complaints():
|
||||
page = get_page_from_request()
|
||||
if page is None:
|
||||
abort(404, "Invalid page argument ({}).".format(request.args.get('page')))
|
||||
abort(404, "Invalid page argument ({}).".format(request.args.get("page")))
|
||||
|
||||
response = complaint_api_client.get_all_complaints(page=page)
|
||||
|
||||
prev_page = None
|
||||
if response['links'].get('prev'):
|
||||
prev_page = generate_previous_dict('main.platform_admin_list_complaints', None, page)
|
||||
if response["links"].get("prev"):
|
||||
prev_page = generate_previous_dict(
|
||||
"main.platform_admin_list_complaints", None, page
|
||||
)
|
||||
next_page = None
|
||||
if response['links'].get('next'):
|
||||
next_page = generate_next_dict('main.platform_admin_list_complaints', None, page)
|
||||
if response["links"].get("next"):
|
||||
next_page = generate_next_dict(
|
||||
"main.platform_admin_list_complaints", None, page
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/platform-admin/complaints.html',
|
||||
complaints=response['complaints'],
|
||||
"views/platform-admin/complaints.html",
|
||||
complaints=response["complaints"],
|
||||
page=page,
|
||||
prev_page=prev_page,
|
||||
next_page=next_page,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/clear-cache", methods=['GET', 'POST'])
|
||||
@main.route("/platform-admin/clear-cache", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def clear_cache():
|
||||
# note: `service-{uuid}-templates` cache is cleared for both services and templates.
|
||||
CACHE_KEYS = OrderedDict([
|
||||
('user', [
|
||||
'user-????????-????-????-????-????????????',
|
||||
]),
|
||||
('service', [
|
||||
'has_jobs-????????-????-????-????-????????????',
|
||||
'service-????????-????-????-????-????????????',
|
||||
'service-????????-????-????-????-????????????-templates',
|
||||
'service-????????-????-????-????-????????????-data-retention',
|
||||
'service-????????-????-????-????-????????????-template-folders',
|
||||
]),
|
||||
('template', [
|
||||
'service-????????-????-????-????-????????????-templates',
|
||||
'service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-version-*',
|
||||
'service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-versions',
|
||||
]),
|
||||
('email_branding', [
|
||||
'email_branding',
|
||||
'email_branding-????????-????-????-????-????????????',
|
||||
]),
|
||||
('organization', [
|
||||
'organizations',
|
||||
'domains',
|
||||
'live-service-and-organization-counts',
|
||||
'organization-????????-????-????-????-????????????-name',
|
||||
]),
|
||||
])
|
||||
CACHE_KEYS = OrderedDict(
|
||||
[
|
||||
(
|
||||
"user",
|
||||
[
|
||||
"user-????????-????-????-????-????????????",
|
||||
],
|
||||
),
|
||||
(
|
||||
"service",
|
||||
[
|
||||
"has_jobs-????????-????-????-????-????????????",
|
||||
"service-????????-????-????-????-????????????",
|
||||
"service-????????-????-????-????-????????????-templates",
|
||||
"service-????????-????-????-????-????????????-data-retention",
|
||||
"service-????????-????-????-????-????????????-template-folders",
|
||||
],
|
||||
),
|
||||
(
|
||||
"template",
|
||||
[
|
||||
"service-????????-????-????-????-????????????-templates",
|
||||
"service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-version-*", # noqa
|
||||
"service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-versions", # noqa
|
||||
],
|
||||
),
|
||||
(
|
||||
"email_branding",
|
||||
[
|
||||
"email_branding",
|
||||
"email_branding-????????-????-????-????-????????????",
|
||||
],
|
||||
),
|
||||
(
|
||||
"organization",
|
||||
[
|
||||
"organizations",
|
||||
"domains",
|
||||
"live-service-and-organization-counts",
|
||||
"organization-????????-????-????-????-????????????-name",
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
form = AdminClearCacheForm()
|
||||
|
||||
form.model_type.choices = [
|
||||
(key, key.replace('_', ' ').title()) for key in CACHE_KEYS
|
||||
(key, key.replace("_", " ").title()) for key in CACHE_KEYS
|
||||
]
|
||||
|
||||
if form.validate_on_submit():
|
||||
@@ -495,83 +627,75 @@ def clear_cache():
|
||||
patterns = list(itertools.chain(*groups))
|
||||
|
||||
num_deleted = sum(
|
||||
redis_client.delete_by_pattern(pattern)
|
||||
for pattern in patterns
|
||||
redis_client.delete_by_pattern(pattern) for pattern in patterns
|
||||
)
|
||||
|
||||
msg = (
|
||||
f'Removed {num_deleted} objects '
|
||||
f'across {len(patterns)} key formats '
|
||||
f"Removed {num_deleted} objects "
|
||||
f"across {len(patterns)} key formats "
|
||||
f'for {", ".join(group_keys)}'
|
||||
)
|
||||
|
||||
flash(msg, category='default')
|
||||
flash(msg, category="default")
|
||||
|
||||
return render_template(
|
||||
'views/platform-admin/clear-cache.html',
|
||||
form=form
|
||||
)
|
||||
return render_template("views/platform-admin/clear-cache.html", form=form)
|
||||
|
||||
|
||||
def sum_service_usage(service):
|
||||
total = 0
|
||||
for notification_type in service['statistics'].keys():
|
||||
total += service['statistics'][notification_type]['requested']
|
||||
for notification_type in service["statistics"].keys():
|
||||
total += service["statistics"][notification_type]["requested"]
|
||||
return total
|
||||
|
||||
|
||||
def filter_and_sort_services(services, trial_mode_services=False):
|
||||
return [
|
||||
service for service in sorted(
|
||||
service
|
||||
for service in sorted(
|
||||
services,
|
||||
key=lambda service: (
|
||||
service['active'],
|
||||
service["active"],
|
||||
sum_service_usage(service),
|
||||
service['created_at']
|
||||
service["created_at"],
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
if service['restricted'] == trial_mode_services
|
||||
if service["restricted"] == trial_mode_services
|
||||
]
|
||||
|
||||
|
||||
def create_global_stats(services):
|
||||
stats = {
|
||||
'email': {
|
||||
'delivered': 0,
|
||||
'failed': 0,
|
||||
'requested': 0
|
||||
},
|
||||
'sms': {
|
||||
'delivered': 0,
|
||||
'failed': 0,
|
||||
'requested': 0
|
||||
},
|
||||
"email": {"delivered": 0, "failed": 0, "requested": 0},
|
||||
"sms": {"delivered": 0, "failed": 0, "requested": 0},
|
||||
}
|
||||
for service in services:
|
||||
for msg_type, status in itertools.product(('sms', 'email'), ('delivered', 'failed', 'requested')):
|
||||
stats[msg_type][status] += service['statistics'][msg_type][status]
|
||||
for msg_type, status in itertools.product(
|
||||
("sms", "email"), ("delivered", "failed", "requested")
|
||||
):
|
||||
stats[msg_type][status] += service["statistics"][msg_type][status]
|
||||
|
||||
for stat in stats.values():
|
||||
stat['failure_rate'] = get_formatted_percentage(stat['failed'], stat['requested'])
|
||||
stat["failure_rate"] = get_formatted_percentage(
|
||||
stat["failed"], stat["requested"]
|
||||
)
|
||||
return stats
|
||||
|
||||
|
||||
def format_stats_by_service(services):
|
||||
for service in services:
|
||||
yield {
|
||||
'id': service['id'],
|
||||
'name': service['name'],
|
||||
'stats': service['statistics'],
|
||||
'restricted': service['restricted'],
|
||||
'research_mode': service['research_mode'],
|
||||
'created_at': service['created_at'],
|
||||
'active': service['active']
|
||||
"id": service["id"],
|
||||
"name": service["name"],
|
||||
"stats": service["statistics"],
|
||||
"restricted": service["restricted"],
|
||||
"research_mode": service["research_mode"],
|
||||
"created_at": service["created_at"],
|
||||
"active": service["active"],
|
||||
}
|
||||
|
||||
|
||||
def _get_user_row(r):
|
||||
|
||||
# [{
|
||||
# 'name': 'Kenneth Kehl',
|
||||
# 'organizations': [],
|
||||
@@ -581,20 +705,20 @@ def _get_user_row(r):
|
||||
# 'platform_admin': True, 'services': ['672b8a66-e22e-40f6-b1e5-39cc1c6bf857'], 'state': 'active'}]
|
||||
|
||||
row = []
|
||||
row.append(r['name'])
|
||||
row.append(r["name"])
|
||||
|
||||
service_id_name_lookup = {}
|
||||
services = []
|
||||
for s in r['services']:
|
||||
for s in r["services"]:
|
||||
my_service = service_api_client.get_service(s)
|
||||
service_id_name_lookup[my_service['data']['id']] = my_service['data']['name']
|
||||
services.append(my_service['data']['name'])
|
||||
service_id_name_lookup[my_service["data"]["id"]] = my_service["data"]["name"]
|
||||
services.append(my_service["data"]["name"])
|
||||
services = str(services)
|
||||
services = services.replace("[", "")
|
||||
services = services.replace("]", "")
|
||||
row.append(services)
|
||||
row.append(r['platform_admin'])
|
||||
permissions = r['permissions']
|
||||
row.append(r["platform_admin"])
|
||||
permissions = r["permissions"]
|
||||
for k, v in service_id_name_lookup.items():
|
||||
if permissions.get(k):
|
||||
permissions[v] = permissions[k]
|
||||
@@ -602,6 +726,6 @@ def _get_user_row(r):
|
||||
|
||||
permissions = json.dumps(permissions, indent=4)
|
||||
row.append(permissions)
|
||||
row.append(r['password_changed_at'])
|
||||
row.append(r['state'])
|
||||
row.append(r["password_changed_at"])
|
||||
row.append(r["state"])
|
||||
return row
|
||||
|
||||
@@ -7,43 +7,46 @@ from app.main.forms import SearchByNameForm
|
||||
from app.main.views.sub_navigation_dictionaries import using_notify_nav
|
||||
from app.utils.user import user_is_logged_in
|
||||
|
||||
CURRENT_SMS_RATE = '1.72'
|
||||
CURRENT_SMS_RATE = "1.72"
|
||||
|
||||
|
||||
@main.route('/using-notify/pricing')
|
||||
@main.route("/using-notify/pricing")
|
||||
@user_is_logged_in
|
||||
def pricing():
|
||||
return render_template(
|
||||
'views/pricing/index.html',
|
||||
"views/pricing/index.html",
|
||||
sms_rate=CURRENT_SMS_RATE,
|
||||
international_sms_rates=sorted([
|
||||
(cc, country['names'], country['billable_units'])
|
||||
for cc, country in INTERNATIONAL_BILLING_RATES.items()
|
||||
], key=lambda x: x[0]),
|
||||
international_sms_rates=sorted(
|
||||
[
|
||||
(cc, country["names"], country["billable_units"])
|
||||
for cc, country in INTERNATIONAL_BILLING_RATES.items()
|
||||
],
|
||||
key=lambda x: x[0],
|
||||
),
|
||||
search_form=SearchByNameForm(),
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/pricing/how-to-pay')
|
||||
@main.route("/pricing/how-to-pay")
|
||||
@user_is_logged_in
|
||||
def how_to_pay():
|
||||
return render_template(
|
||||
'views/pricing/how-to-pay.html',
|
||||
"views/pricing/how-to-pay.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/pricing/billing-details')
|
||||
@main.route("/pricing/billing-details")
|
||||
@user_is_logged_in
|
||||
def billing_details():
|
||||
if current_user.is_authenticated:
|
||||
return render_template(
|
||||
'views/pricing/billing-details.html',
|
||||
billing_details=current_app.config['NOTIFY_BILLING_DETAILS'],
|
||||
"views/pricing/billing-details.html",
|
||||
billing_details=current_app.config["NOTIFY_BILLING_DETAILS"],
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
return render_template(
|
||||
'views/pricing/billing-details-signed-out.html',
|
||||
"views/pricing/billing-details-signed-out.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
@@ -14,55 +14,59 @@ PROVIDER_PRIORITY_MEANING_SWITCHOVER = datetime(2019, 11, 29, 11, 0).isoformat()
|
||||
@main.route("/providers")
|
||||
@user_is_platform_admin
|
||||
def view_providers():
|
||||
providers = provider_client.get_all_providers()['provider_details']
|
||||
providers = provider_client.get_all_providers()["provider_details"]
|
||||
domestic_email_providers, domestic_sms_providers, intl_sms_providers = [], [], []
|
||||
for provider in providers:
|
||||
if provider['notification_type'] == 'sms':
|
||||
if provider["notification_type"] == "sms":
|
||||
domestic_sms_providers.append(provider)
|
||||
if provider.get('supports_international', None):
|
||||
if provider.get("supports_international", None):
|
||||
intl_sms_providers.append(provider)
|
||||
elif provider['notification_type'] == 'email':
|
||||
elif provider["notification_type"] == "email":
|
||||
domestic_email_providers.append(provider)
|
||||
|
||||
add_monthly_traffic(domestic_sms_providers)
|
||||
|
||||
return render_template(
|
||||
'views/providers/providers.html',
|
||||
"views/providers/providers.html",
|
||||
email_providers=domestic_email_providers,
|
||||
domestic_sms_providers=domestic_sms_providers,
|
||||
intl_sms_providers=intl_sms_providers
|
||||
intl_sms_providers=intl_sms_providers,
|
||||
)
|
||||
|
||||
|
||||
def add_monthly_traffic(domestic_sms_providers):
|
||||
total_sms_sent = sum(provider['current_month_billable_sms'] for provider in domestic_sms_providers)
|
||||
total_sms_sent = sum(
|
||||
provider["current_month_billable_sms"] for provider in domestic_sms_providers
|
||||
)
|
||||
|
||||
for provider in domestic_sms_providers:
|
||||
percentage = (provider['current_month_billable_sms'] / total_sms_sent * 100) if total_sms_sent else 0
|
||||
provider['monthly_traffic'] = round(percentage)
|
||||
percentage = (
|
||||
(provider["current_month_billable_sms"] / total_sms_sent * 100)
|
||||
if total_sms_sent
|
||||
else 0
|
||||
)
|
||||
provider["monthly_traffic"] = round(percentage)
|
||||
|
||||
|
||||
@main.route("/provider/edit-sms-provider-ratio", methods=['GET', 'POST'])
|
||||
@main.route("/provider/edit-sms-provider-ratio", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_sms_provider_ratio():
|
||||
providers = [
|
||||
provider
|
||||
for provider in provider_client.get_all_providers()['provider_details']
|
||||
if provider['notification_type'] == 'sms' and provider['active']
|
||||
for provider in provider_client.get_all_providers()["provider_details"]
|
||||
if provider["notification_type"] == "sms" and provider["active"]
|
||||
]
|
||||
|
||||
form = AdminProviderRatioForm(providers)
|
||||
|
||||
if form.validate_on_submit():
|
||||
for provider in providers:
|
||||
field = getattr(form, provider['identifier'])
|
||||
provider_client.update_provider(provider['id'], field.data)
|
||||
return redirect(url_for('.view_providers'))
|
||||
field = getattr(form, provider["identifier"])
|
||||
provider_client.update_provider(provider["id"], field.data)
|
||||
return redirect(url_for(".view_providers"))
|
||||
|
||||
return render_template(
|
||||
'views/providers/edit-sms-provider-ratio.html',
|
||||
form=form,
|
||||
providers=providers
|
||||
"views/providers/edit-sms-provider-ratio.html", form=form, providers=providers
|
||||
)
|
||||
|
||||
|
||||
@@ -70,4 +74,6 @@ def edit_sms_provider_ratio():
|
||||
@user_is_platform_admin
|
||||
def view_provider(provider_id):
|
||||
versions = provider_client.get_provider_versions(provider_id)
|
||||
return render_template('views/providers/provider.html', provider_versions=versions['data'])
|
||||
return render_template(
|
||||
"views/providers/provider.html", provider_versions=versions["data"]
|
||||
)
|
||||
|
||||
@@ -14,21 +14,21 @@ from app.models.user import InvitedOrgUser, InvitedUser, User
|
||||
from app.utils import hide_from_search_engines
|
||||
|
||||
|
||||
@main.route('/register', methods=['GET', 'POST'])
|
||||
@main.route("/register", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def register():
|
||||
if current_user and current_user.is_authenticated:
|
||||
return redirect(url_for('main.show_accounts_or_dashboard'))
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
|
||||
form = RegisterUserForm()
|
||||
if form.validate_on_submit():
|
||||
_do_registration(form, send_sms=False)
|
||||
return redirect(url_for('main.registration_continue'))
|
||||
return redirect(url_for("main.registration_continue"))
|
||||
|
||||
return render_template('views/register.html', form=form)
|
||||
return render_template("views/register.html", form=form)
|
||||
|
||||
|
||||
@main.route('/register-from-invite', methods=['GET', 'POST'])
|
||||
@main.route("/register-from-invite", methods=["GET", "POST"])
|
||||
def register_from_invite():
|
||||
invited_user = InvitedUser.from_session()
|
||||
if not invited_user:
|
||||
@@ -37,21 +37,26 @@ def register_from_invite():
|
||||
form = RegisterUserFromInviteForm(invited_user)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.service.data != invited_user.service or form.email_address.data != invited_user.email_address:
|
||||
if (
|
||||
form.service.data != invited_user.service
|
||||
or form.email_address.data != invited_user.email_address
|
||||
):
|
||||
abort(400)
|
||||
_do_registration(form, send_email=False, send_sms=invited_user.sms_auth)
|
||||
invited_user.accept_invite()
|
||||
if invited_user.sms_auth:
|
||||
return redirect(url_for('main.verify'))
|
||||
return redirect(url_for("main.verify"))
|
||||
else:
|
||||
# we've already proven this user has email because they clicked the invite link,
|
||||
# so just activate them straight away
|
||||
return activate_user(session['user_details']['id'])
|
||||
return activate_user(session["user_details"]["id"])
|
||||
|
||||
return render_template('views/register-from-invite.html', invited_user=invited_user, form=form)
|
||||
return render_template(
|
||||
"views/register-from-invite.html", invited_user=invited_user, form=form
|
||||
)
|
||||
|
||||
|
||||
@main.route('/register-from-org-invite', methods=['GET', 'POST'])
|
||||
@main.route("/register-from-org-invite", methods=["GET", "POST"])
|
||||
def register_from_org_invite():
|
||||
invited_org_user = InvitedOrgUser.from_session()
|
||||
if not invited_org_user:
|
||||
@@ -60,17 +65,28 @@ def register_from_org_invite():
|
||||
form = RegisterUserFromOrgInviteForm(
|
||||
invited_org_user,
|
||||
)
|
||||
form.auth_type.data = 'sms_auth'
|
||||
form.auth_type.data = "sms_auth"
|
||||
|
||||
if form.validate_on_submit():
|
||||
if (form.organization.data != invited_org_user.organization or
|
||||
form.email_address.data != invited_org_user.email_address):
|
||||
if (
|
||||
form.organization.data != invited_org_user.organization
|
||||
or form.email_address.data != invited_org_user.email_address
|
||||
):
|
||||
abort(400)
|
||||
_do_registration(form, send_email=False, send_sms=True, organization_id=invited_org_user.organization)
|
||||
_do_registration(
|
||||
form,
|
||||
send_email=False,
|
||||
send_sms=True,
|
||||
organization_id=invited_org_user.organization,
|
||||
)
|
||||
invited_org_user.accept_invite()
|
||||
|
||||
return redirect(url_for('main.verify'))
|
||||
return render_template('views/register-from-org-invite.html', invited_org_user=invited_org_user, form=form)
|
||||
return redirect(url_for("main.verify"))
|
||||
return render_template(
|
||||
"views/register-from-org-invite.html",
|
||||
invited_org_user=invited_org_user,
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
def _do_registration(form, send_sms=True, send_email=True, organization_id=None):
|
||||
@@ -78,8 +94,8 @@ def _do_registration(form, send_sms=True, send_email=True, organization_id=None)
|
||||
if user:
|
||||
if send_email:
|
||||
user.send_already_registered_email()
|
||||
session['expiry_date'] = str(datetime.utcnow() + timedelta(hours=1))
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
session["expiry_date"] = str(datetime.utcnow() + timedelta(hours=1))
|
||||
session["user_details"] = {"email": user.email_address, "id": user.id}
|
||||
else:
|
||||
user = User.register(
|
||||
name=form.name.data,
|
||||
@@ -94,14 +110,14 @@ def _do_registration(form, send_sms=True, send_email=True, organization_id=None)
|
||||
|
||||
if send_sms:
|
||||
user.send_verify_code()
|
||||
session['expiry_date'] = str(datetime.utcnow() + timedelta(hours=1))
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
session["expiry_date"] = str(datetime.utcnow() + timedelta(hours=1))
|
||||
session["user_details"] = {"email": user.email_address, "id": user.id}
|
||||
if organization_id:
|
||||
session['organization_id'] = organization_id
|
||||
session["organization_id"] = organization_id
|
||||
|
||||
|
||||
@main.route('/registration-continue')
|
||||
@main.route("/registration-continue")
|
||||
def registration_continue():
|
||||
if not session.get('user_details'):
|
||||
return redirect(url_for('.show_accounts_or_dashboard'))
|
||||
return render_template('views/registration-continue.html')
|
||||
if not session.get("user_details"):
|
||||
return redirect(url_for(".show_accounts_or_dashboard"))
|
||||
return render_template("views/registration-continue.html")
|
||||
|
||||
@@ -3,8 +3,8 @@ from flask import redirect
|
||||
from app.main import main
|
||||
|
||||
|
||||
@main.route('/.well-known/security.txt', methods=['GET'])
|
||||
@main.route('/security.txt', methods=['GET'])
|
||||
@main.route("/.well-known/security.txt", methods=["GET"])
|
||||
@main.route("/security.txt", methods=["GET"])
|
||||
def security_policy():
|
||||
# See GDS Way security policy which this implements
|
||||
# https://gds-way.cloudapps.digital/standards/vulnerability-disclosure.html#vulnerability-disclosure-and-security-txt
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -18,37 +18,38 @@ from app.utils import hide_from_search_engines
|
||||
from app.utils.login import is_safe_redirect_url
|
||||
|
||||
|
||||
@main.route('/sign-in', methods=(['GET', 'POST']))
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in():
|
||||
redirect_url = request.args.get('next')
|
||||
redirect_url = request.args.get("next")
|
||||
if current_user and current_user.is_authenticated:
|
||||
if redirect_url and is_safe_redirect_url(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
return redirect(url_for('main.show_accounts_or_dashboard'))
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
|
||||
form = LoginForm()
|
||||
password_reset_url = url_for('.forgot_password', next=request.args.get('next'))
|
||||
password_reset_url = url_for(".forgot_password", next=request.args.get("next"))
|
||||
|
||||
if form.validate_on_submit():
|
||||
|
||||
user = User.from_email_address_and_password_or_none(
|
||||
form.email_address.data, form.password.data
|
||||
)
|
||||
|
||||
if user:
|
||||
# add user to session to mark us as in the process of signing the user in
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
session["user_details"] = {"email": user.email_address, "id": user.id}
|
||||
|
||||
if user.state == 'pending':
|
||||
return redirect(url_for('main.resend_email_verification', next=redirect_url))
|
||||
if user.state == "pending":
|
||||
return redirect(
|
||||
url_for("main.resend_email_verification", next=redirect_url)
|
||||
)
|
||||
|
||||
if user.is_active:
|
||||
if session.get('invited_user_id'):
|
||||
if session.get("invited_user_id"):
|
||||
invited_user = InvitedUser.from_session()
|
||||
if user.email_address.lower() != invited_user.email_address.lower():
|
||||
flash("You cannot accept an invite for another person.")
|
||||
session.pop('invited_user_id', None)
|
||||
session.pop("invited_user_id", None)
|
||||
abort(403)
|
||||
else:
|
||||
invited_user.accept_invite()
|
||||
@@ -56,30 +57,32 @@ def sign_in():
|
||||
user.send_login_code()
|
||||
|
||||
if user.sms_auth:
|
||||
return redirect(url_for('.two_factor_sms', next=redirect_url))
|
||||
return redirect(url_for(".two_factor_sms", next=redirect_url))
|
||||
if user.email_auth:
|
||||
return redirect(url_for('.two_factor_email_sent', next=redirect_url))
|
||||
return redirect(
|
||||
url_for(".two_factor_email_sent", next=redirect_url)
|
||||
)
|
||||
|
||||
# Vague error message for login in case of user not known, locked, inactive or password not verified
|
||||
flash(Markup(
|
||||
(
|
||||
f"The email address or password you entered is incorrect."
|
||||
f" <a href={password_reset_url} class='usa-link'>Forgot your password?</a>"
|
||||
flash(
|
||||
Markup(
|
||||
(
|
||||
f"The email address or password you entered is incorrect."
|
||||
f" <a href={password_reset_url} class='usa-link'>Forgot your password?</a>"
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
other_device = current_user.logged_in_elsewhere()
|
||||
return render_template(
|
||||
'views/signin.html',
|
||||
"views/signin.html",
|
||||
form=form,
|
||||
again=bool(redirect_url),
|
||||
other_device=other_device,
|
||||
password_reset_url=password_reset_url
|
||||
password_reset_url=password_reset_url,
|
||||
)
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def sign_in_again():
|
||||
return redirect(
|
||||
url_for('main.sign_in', next=request.path)
|
||||
)
|
||||
return redirect(url_for("main.sign_in", next=request.path))
|
||||
|
||||
@@ -4,9 +4,9 @@ from flask_login import current_user
|
||||
from app.main import main
|
||||
|
||||
|
||||
@main.route('/sign-out', methods=(['GET']))
|
||||
@main.route("/sign-out", methods=(["GET"]))
|
||||
def sign_out():
|
||||
# An AnonymousUser does not have an id
|
||||
if current_user.is_authenticated:
|
||||
current_user.sign_out()
|
||||
return redirect(url_for('main.index'))
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
@@ -12,7 +12,7 @@ def features_nav():
|
||||
# "name": "Text messages",
|
||||
# "link": "main.features_sms",
|
||||
# },
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Roadmap",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,11 +14,13 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/tour/<uuid:template_id>")
|
||||
@user_has_permissions('send_messages')
|
||||
@user_has_permissions("send_messages")
|
||||
def begin_tour(service_id, template_id):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(template_id, current_user)
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
|
||||
if (db_template['template_type'] != 'sms' or not current_user.mobile_number):
|
||||
if db_template["template_type"] != "sms" or not current_user.mobile_number:
|
||||
abort(404)
|
||||
|
||||
template = get_template(
|
||||
@@ -29,31 +31,37 @@ def begin_tour(service_id, template_id):
|
||||
|
||||
template.values = {"phone_number": current_user.mobile_number}
|
||||
|
||||
session['placeholders'] = {}
|
||||
session["placeholders"] = {}
|
||||
|
||||
return render_template(
|
||||
'views/templates/start-tour.html',
|
||||
"views/templates/start-tour.html",
|
||||
template=template,
|
||||
help='1',
|
||||
continue_link=url_for('.tour_step', service_id=service_id, template_id=template_id, step_index=1)
|
||||
help="1",
|
||||
continue_link=url_for(
|
||||
".tour_step", service_id=service_id, template_id=template_id, step_index=1
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/tour/<uuid:template_id>/step-<int:step_index>",
|
||||
methods=['GET', 'POST'],
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions('send_messages', restrict_admin_usage=True)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def tour_step(service_id, template_id, step_index):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(template_id, current_user)
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
|
||||
if (db_template['template_type'] != 'sms' or step_index == 0):
|
||||
if db_template["template_type"] != "sms" or step_index == 0:
|
||||
abort(404)
|
||||
|
||||
if 'placeholders' not in session:
|
||||
return redirect(url_for(
|
||||
'.begin_tour', service_id=current_service.id, template_id=template_id
|
||||
))
|
||||
if "placeholders" not in session:
|
||||
return redirect(
|
||||
url_for(
|
||||
".begin_tour", service_id=current_service.id, template_id=template_id
|
||||
)
|
||||
)
|
||||
|
||||
template = get_template(
|
||||
db_template,
|
||||
@@ -67,57 +75,88 @@ def tour_step(service_id, template_id, step_index):
|
||||
current_placeholder = placeholders[step_index - 1]
|
||||
except IndexError:
|
||||
if all_placeholders_in_session(placeholders):
|
||||
return redirect(url_for(
|
||||
'.check_tour_notification', service_id=current_service.id, template_id=template_id
|
||||
))
|
||||
return redirect(url_for(
|
||||
'.tour_step', service_id=current_service.id, template_id=template_id, step_index=1
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".check_tour_notification",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
".tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=1,
|
||||
)
|
||||
)
|
||||
|
||||
form = get_placeholder_form_instance(
|
||||
current_placeholder,
|
||||
dict_to_populate_from=get_normalised_placeholders_from_session(),
|
||||
template_type=template.template_type,
|
||||
allow_international_phone_numbers=current_service.has_permission('international_sms')
|
||||
allow_international_phone_numbers=current_service.has_permission(
|
||||
"international_sms"
|
||||
),
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
session['placeholders'][current_placeholder] = form.placeholder_value.data
|
||||
session["placeholders"][current_placeholder] = form.placeholder_value.data
|
||||
|
||||
if all_placeholders_in_session(placeholders):
|
||||
return redirect(url_for(
|
||||
'.check_tour_notification', service_id=current_service.id, template_id=template_id
|
||||
))
|
||||
return redirect(url_for(
|
||||
'.tour_step', service_id=current_service.id, template_id=template_id, step_index=step_index + 1
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".check_tour_notification",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
".tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=step_index + 1,
|
||||
)
|
||||
)
|
||||
|
||||
back_link = _get_tour_step_back_link(service_id, template_id, step_index)
|
||||
|
||||
template.values = get_recipient_and_placeholders_from_session(db_template['template_type'])
|
||||
template.values = get_recipient_and_placeholders_from_session(
|
||||
db_template["template_type"]
|
||||
)
|
||||
template.values[current_placeholder] = None
|
||||
|
||||
return render_template(
|
||||
'views/send-test.html',
|
||||
"views/send-test.html",
|
||||
page_title="Example text message",
|
||||
template=template,
|
||||
form=form,
|
||||
back_link=back_link,
|
||||
help='2'
|
||||
help="2",
|
||||
)
|
||||
|
||||
|
||||
def _get_tour_step_back_link(service_id, template_id, step_index):
|
||||
if step_index == 1:
|
||||
return url_for('.begin_tour', service_id=service_id, template_id=template_id)
|
||||
return url_for(".begin_tour", service_id=service_id, template_id=template_id)
|
||||
|
||||
return url_for('.tour_step', service_id=service_id, template_id=template_id, step_index=step_index - 1)
|
||||
return url_for(
|
||||
".tour_step",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=step_index - 1,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/tour/<uuid:template_id>/check", methods=['GET'])
|
||||
@user_has_permissions('send_messages', restrict_admin_usage=True)
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/tour/<uuid:template_id>/check", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def check_tour_notification(service_id, template_id):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(template_id, current_user)
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
|
||||
template = get_template(
|
||||
db_template,
|
||||
@@ -125,38 +164,47 @@ def check_tour_notification(service_id, template_id):
|
||||
show_recipient=True,
|
||||
)
|
||||
|
||||
if 'placeholders' not in session:
|
||||
return redirect(url_for(
|
||||
'.begin_tour', service_id=current_service.id, template_id=template_id
|
||||
))
|
||||
if "placeholders" not in session:
|
||||
return redirect(
|
||||
url_for(
|
||||
".begin_tour", service_id=current_service.id, template_id=template_id
|
||||
)
|
||||
)
|
||||
|
||||
placeholders = fields_to_fill_in(template, prefill_current_user=True)
|
||||
|
||||
if not all_placeholders_in_session(template.placeholders):
|
||||
return redirect(url_for(
|
||||
'.tour_step', service_id=current_service.id, template_id=template_id, step_index=1
|
||||
))
|
||||
return redirect(
|
||||
url_for(
|
||||
".tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=1,
|
||||
)
|
||||
)
|
||||
|
||||
back_link = url_for(
|
||||
'.tour_step', service_id=current_service.id, template_id=template_id, step_index=len(placeholders)
|
||||
".tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=len(placeholders),
|
||||
)
|
||||
|
||||
template.values = get_recipient_and_placeholders_from_session(template.template_type)
|
||||
template.values = get_recipient_and_placeholders_from_session(
|
||||
template.template_type
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/notifications/check.html',
|
||||
"views/notifications/check.html",
|
||||
template=template,
|
||||
back_link=back_link,
|
||||
help='2',
|
||||
help="2",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/end-tour/<uuid:example_template_id>")
|
||||
@user_has_permissions('manage_templates')
|
||||
@user_has_permissions("manage_templates")
|
||||
def go_to_dashboard_after_tour(service_id, example_template_id):
|
||||
|
||||
service_api_client.delete_service_template(service_id, example_template_id)
|
||||
|
||||
return redirect(
|
||||
url_for('main.service_dashboard', service_id=service_id)
|
||||
)
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
|
||||
@@ -17,71 +17,83 @@ from app.utils.login import (
|
||||
)
|
||||
|
||||
|
||||
@main.route('/two-factor-email-sent', methods=['GET'])
|
||||
@main.route("/two-factor-email-sent", methods=["GET"])
|
||||
def two_factor_email_sent():
|
||||
title = 'Email resent' if request.args.get('email_resent') else 'Check your email'
|
||||
title = "Email resent" if request.args.get("email_resent") else "Check your email"
|
||||
return render_template(
|
||||
'views/two-factor-email.html',
|
||||
"views/two-factor-email.html",
|
||||
title=title,
|
||||
redirect_url=request.args.get('next')
|
||||
redirect_url=request.args.get("next"),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/email-auth/<token>', methods=['GET'])
|
||||
@main.route("/email-auth/<token>", methods=["GET"])
|
||||
def two_factor_email_interstitial(token):
|
||||
return render_template('views/email-link-interstitial.html')
|
||||
return render_template("views/email-link-interstitial.html")
|
||||
|
||||
|
||||
@main.route('/email-auth/<token>', methods=['POST'])
|
||||
@main.route("/email-auth/<token>", methods=["POST"])
|
||||
def two_factor_email(token):
|
||||
redirect_url = request.args.get('next')
|
||||
redirect_url = request.args.get("next")
|
||||
if current_user.is_authenticated:
|
||||
return redirect_when_logged_in(platform_admin=current_user.platform_admin)
|
||||
|
||||
# checks url is valid, and hasn't timed out
|
||||
try:
|
||||
token_data = json.loads(check_token(
|
||||
token,
|
||||
current_app.config['SECRET_KEY'],
|
||||
current_app.config['DANGEROUS_SALT'],
|
||||
current_app.config['EMAIL_EXPIRY_SECONDS']
|
||||
))
|
||||
token_data = json.loads(
|
||||
check_token(
|
||||
token,
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
current_app.config["EMAIL_EXPIRY_SECONDS"],
|
||||
)
|
||||
)
|
||||
except SignatureExpired:
|
||||
return render_template('views/email-link-invalid.html', redirect_url=redirect_url)
|
||||
return render_template(
|
||||
"views/email-link-invalid.html", redirect_url=redirect_url
|
||||
)
|
||||
|
||||
user_id = token_data['user_id']
|
||||
user_id = token_data["user_id"]
|
||||
# checks if code was already used
|
||||
logged_in, msg = user_api_client.check_verify_code(user_id, token_data['secret_code'], "email")
|
||||
logged_in, msg = user_api_client.check_verify_code(
|
||||
user_id, token_data["secret_code"], "email"
|
||||
)
|
||||
|
||||
if not logged_in:
|
||||
return render_template('views/email-link-invalid.html', redirect_url=redirect_url)
|
||||
return render_template(
|
||||
"views/email-link-invalid.html", redirect_url=redirect_url
|
||||
)
|
||||
return log_in_user(user_id)
|
||||
|
||||
|
||||
@main.route('/two-factor-sms', methods=['GET', 'POST'])
|
||||
@main.route("/two-factor-sms", methods=["GET", "POST"])
|
||||
@redirect_to_sign_in
|
||||
def two_factor_sms():
|
||||
user_id = session['user_details']['id']
|
||||
user_id = session["user_details"]["id"]
|
||||
user = User.from_id(user_id)
|
||||
|
||||
def _check_code(code):
|
||||
return user_api_client.check_verify_code(user_id, code, "sms")
|
||||
|
||||
form = TwoFactorForm(_check_code)
|
||||
redirect_url = request.args.get('next')
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if form.validate_on_submit():
|
||||
if email_needs_revalidating(user):
|
||||
user_api_client.send_verify_code(user.id, 'email', None, redirect_url)
|
||||
return redirect(url_for('.revalidate_email_sent', next=redirect_url))
|
||||
user_api_client.send_verify_code(user.id, "email", None, redirect_url)
|
||||
return redirect(url_for(".revalidate_email_sent", next=redirect_url))
|
||||
else:
|
||||
return log_in_user(user_id)
|
||||
|
||||
return render_template('views/two-factor-sms.html', form=form, redirect_url=redirect_url)
|
||||
return render_template(
|
||||
"views/two-factor-sms.html", form=form, redirect_url=redirect_url
|
||||
)
|
||||
|
||||
|
||||
@main.route('/re-validate-email', methods=['GET'])
|
||||
@main.route("/re-validate-email", methods=["GET"])
|
||||
def revalidate_email_sent():
|
||||
title = 'Email resent' if request.args.get('email_resent') else 'Check your email'
|
||||
redirect_url = request.args.get('next')
|
||||
return render_template('views/re-validate-email-sent.html', title=title, redirect_url=redirect_url)
|
||||
title = "Email resent" if request.args.get("email_resent") else "Check your email"
|
||||
redirect_url = request.args.get("next")
|
||||
return render_template(
|
||||
"views/re-validate-email-sent.html", title=title, redirect_url=redirect_url
|
||||
)
|
||||
|
||||
@@ -15,25 +15,24 @@ MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
||||
def uploads(service_id):
|
||||
# No tests have been written, this has been quickly prepared for user research.
|
||||
# It's also very like that a new view will be created to show uploads.
|
||||
uploads = current_service.get_page_of_uploads(page=request.args.get('page'))
|
||||
uploads = current_service.get_page_of_uploads(page=request.args.get("page"))
|
||||
|
||||
prev_page = None
|
||||
if uploads.prev_page:
|
||||
prev_page = generate_previous_dict('main.uploads', service_id, uploads.current_page)
|
||||
prev_page = generate_previous_dict(
|
||||
"main.uploads", service_id, uploads.current_page
|
||||
)
|
||||
next_page = None
|
||||
if uploads.next_page:
|
||||
next_page = generate_next_dict('main.uploads', service_id, uploads.current_page)
|
||||
next_page = generate_next_dict("main.uploads", service_id, uploads.current_page)
|
||||
|
||||
if uploads.current_page == 1:
|
||||
listed_uploads = (
|
||||
current_service.scheduled_jobs +
|
||||
uploads
|
||||
)
|
||||
listed_uploads = current_service.scheduled_jobs + uploads
|
||||
else:
|
||||
listed_uploads = uploads
|
||||
|
||||
return render_template(
|
||||
'views/jobs/jobs.html',
|
||||
"views/jobs/jobs.html",
|
||||
jobs=listed_uploads,
|
||||
prev_page=prev_page,
|
||||
next_page=next_page,
|
||||
|
||||
@@ -31,149 +31,155 @@ from app.main.forms import (
|
||||
from app.models.user import User
|
||||
from app.utils.user import user_is_gov_user, user_is_logged_in
|
||||
|
||||
NEW_EMAIL = 'new-email'
|
||||
NEW_MOBILE = 'new-mob'
|
||||
NEW_MOBILE_PASSWORD_CONFIRMED = 'new-mob-password-confirmed' # nosec B105 - this is not a password
|
||||
NEW_EMAIL = "new-email"
|
||||
NEW_MOBILE = "new-mob"
|
||||
NEW_MOBILE_PASSWORD_CONFIRMED = (
|
||||
"new-mob-password-confirmed" # nosec B105 - this is not a password
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile")
|
||||
@user_is_logged_in
|
||||
def user_profile():
|
||||
return render_template(
|
||||
'views/user-profile.html',
|
||||
"views/user-profile.html",
|
||||
can_see_edit=current_user.is_gov_user,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/name", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/name", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_name():
|
||||
|
||||
form = ChangeNameForm(new_name=current_user.name)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_user.update(name=form.new_name.data)
|
||||
return redirect(url_for('.user_profile'))
|
||||
return redirect(url_for(".user_profile"))
|
||||
|
||||
return render_template(
|
||||
'views/user-profile/change.html',
|
||||
thing='name',
|
||||
form_field=form.new_name
|
||||
"views/user-profile/change.html", thing="name", form_field=form.new_name
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/email", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/email", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
@user_is_gov_user
|
||||
def user_profile_email():
|
||||
|
||||
form = ChangeEmailForm(User.already_registered,
|
||||
email_address=current_user.email_address)
|
||||
form = ChangeEmailForm(
|
||||
User.already_registered, email_address=current_user.email_address
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
session[NEW_EMAIL] = form.email_address.data
|
||||
return redirect(url_for('.user_profile_email_authenticate'))
|
||||
return redirect(url_for(".user_profile_email_authenticate"))
|
||||
return render_template(
|
||||
'views/user-profile/change.html',
|
||||
thing='email address',
|
||||
form_field=form.email_address
|
||||
"views/user-profile/change.html",
|
||||
thing="email address",
|
||||
form_field=form.email_address,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/email/authenticate", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/email/authenticate", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_email_authenticate():
|
||||
# Validate password for form
|
||||
def _check_password(pwd):
|
||||
return user_api_client.verify_password(current_user.id, pwd)
|
||||
|
||||
form = ConfirmPasswordForm(_check_password)
|
||||
|
||||
if NEW_EMAIL not in session:
|
||||
return redirect('main.user_profile_email')
|
||||
return redirect("main.user_profile_email")
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_api_client.send_change_email_verification(current_user.id, session[NEW_EMAIL])
|
||||
user_api_client.send_change_email_verification(
|
||||
current_user.id, session[NEW_EMAIL]
|
||||
)
|
||||
create_email_change_event(
|
||||
user_id=current_user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_email_address=current_user.email_address,
|
||||
new_email_address=session[NEW_EMAIL],
|
||||
)
|
||||
return render_template('views/change-email-continue.html',
|
||||
new_email=session[NEW_EMAIL])
|
||||
return render_template(
|
||||
"views/change-email-continue.html", new_email=session[NEW_EMAIL]
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/user-profile/authenticate.html',
|
||||
thing='email address',
|
||||
"views/user-profile/authenticate.html",
|
||||
thing="email address",
|
||||
form=form,
|
||||
back_link=url_for('.user_profile_email')
|
||||
back_link=url_for(".user_profile_email"),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/email/confirm/<token>", methods=['GET'])
|
||||
@main.route("/user-profile/email/confirm/<token>", methods=["GET"])
|
||||
@user_is_logged_in
|
||||
def user_profile_email_confirm(token):
|
||||
token_data = check_token(token,
|
||||
current_app.config['SECRET_KEY'],
|
||||
current_app.config['DANGEROUS_SALT'],
|
||||
current_app.config['EMAIL_EXPIRY_SECONDS'])
|
||||
token_data = check_token(
|
||||
token,
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
current_app.config["EMAIL_EXPIRY_SECONDS"],
|
||||
)
|
||||
token_data = json.loads(token_data)
|
||||
user = User.from_id(token_data['user_id'])
|
||||
user.update(email_address=token_data['email'])
|
||||
user = User.from_id(token_data["user_id"])
|
||||
user.update(email_address=token_data["email"])
|
||||
session.pop(NEW_EMAIL, None)
|
||||
|
||||
return redirect(url_for('.user_profile'))
|
||||
return redirect(url_for(".user_profile"))
|
||||
|
||||
|
||||
@main.route("/user-profile/mobile-number", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/mobile-number", methods=["GET", "POST"])
|
||||
@main.route(
|
||||
"/user-profile/mobile-number/delete",
|
||||
methods=['GET'],
|
||||
endpoint="user_profile_confirm_delete_mobile_number"
|
||||
methods=["GET"],
|
||||
endpoint="user_profile_confirm_delete_mobile_number",
|
||||
)
|
||||
@user_is_logged_in
|
||||
def user_profile_mobile_number():
|
||||
|
||||
user = User.from_id(current_user.id)
|
||||
form = ChangeMobileNumberForm(mobile_number=current_user.mobile_number)
|
||||
|
||||
if form.validate_on_submit():
|
||||
session[NEW_MOBILE] = form.mobile_number.data
|
||||
return redirect(url_for('.user_profile_mobile_number_authenticate'))
|
||||
return redirect(url_for(".user_profile_mobile_number_authenticate"))
|
||||
|
||||
if (request.endpoint == "main.user_profile_confirm_delete_mobile_number"):
|
||||
flash("Are you sure you want to delete your mobile number from Notify?", 'delete')
|
||||
if request.endpoint == "main.user_profile_confirm_delete_mobile_number":
|
||||
flash(
|
||||
"Are you sure you want to delete your mobile number from Notify?", "delete"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'views/user-profile/change.html',
|
||||
thing='mobile number',
|
||||
"views/user-profile/change.html",
|
||||
thing="mobile number",
|
||||
form_field=form.mobile_number,
|
||||
user_auth=user.auth_type
|
||||
user_auth=user.auth_type,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/mobile-number/delete", methods=['POST'])
|
||||
@main.route("/user-profile/mobile-number/delete", methods=["POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_mobile_number_delete():
|
||||
if current_user.auth_type != 'email_auth':
|
||||
if current_user.auth_type != "email_auth":
|
||||
abort(403)
|
||||
|
||||
current_user.update(mobile_number=None)
|
||||
|
||||
return redirect(url_for('.user_profile'))
|
||||
return redirect(url_for(".user_profile"))
|
||||
|
||||
|
||||
@main.route("/user-profile/mobile-number/authenticate", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/mobile-number/authenticate", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_mobile_number_authenticate():
|
||||
|
||||
# Validate password for form
|
||||
def _check_password(pwd):
|
||||
return user_api_client.verify_password(current_user.id, pwd)
|
||||
|
||||
form = ConfirmPasswordForm(_check_password)
|
||||
|
||||
if NEW_MOBILE not in session:
|
||||
return redirect(url_for('.user_profile_mobile_number'))
|
||||
return redirect(url_for(".user_profile_mobile_number"))
|
||||
|
||||
if form.validate_on_submit():
|
||||
session[NEW_MOBILE_PASSWORD_CONFIRMED] = True
|
||||
@@ -182,28 +188,27 @@ def user_profile_mobile_number_authenticate():
|
||||
user_id=current_user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_mobile_number=current_user.mobile_number,
|
||||
new_mobile_number=session[NEW_MOBILE]
|
||||
new_mobile_number=session[NEW_MOBILE],
|
||||
)
|
||||
return redirect(url_for('.user_profile_mobile_number_confirm'))
|
||||
return redirect(url_for(".user_profile_mobile_number_confirm"))
|
||||
|
||||
return render_template(
|
||||
'views/user-profile/authenticate.html',
|
||||
thing='mobile number',
|
||||
"views/user-profile/authenticate.html",
|
||||
thing="mobile number",
|
||||
form=form,
|
||||
back_link=url_for('.user_profile_mobile_number_confirm')
|
||||
back_link=url_for(".user_profile_mobile_number_confirm"),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/mobile-number/confirm", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/mobile-number/confirm", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_mobile_number_confirm():
|
||||
|
||||
# Validate verify code for form
|
||||
def _check_code(cde):
|
||||
return user_api_client.check_verify_code(current_user.id, cde, 'sms')
|
||||
return user_api_client.check_verify_code(current_user.id, cde, "sms")
|
||||
|
||||
if NEW_MOBILE_PASSWORD_CONFIRMED not in session:
|
||||
return redirect(url_for('.user_profile_mobile_number'))
|
||||
return redirect(url_for(".user_profile_mobile_number"))
|
||||
|
||||
form = TwoFactorForm(_check_code)
|
||||
|
||||
@@ -213,54 +218,56 @@ def user_profile_mobile_number_confirm():
|
||||
del session[NEW_MOBILE]
|
||||
del session[NEW_MOBILE_PASSWORD_CONFIRMED]
|
||||
current_user.update(mobile_number=mobile_number)
|
||||
return redirect(url_for('.user_profile'))
|
||||
return redirect(url_for(".user_profile"))
|
||||
|
||||
return render_template(
|
||||
'views/user-profile/confirm.html',
|
||||
"views/user-profile/confirm.html",
|
||||
form_field=form.sms_code,
|
||||
thing='mobile number'
|
||||
thing="mobile number",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/password", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/password", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_password():
|
||||
|
||||
# Validate password for form
|
||||
def _check_password(pwd):
|
||||
return user_api_client.verify_password(current_user.id, pwd)
|
||||
|
||||
form = ChangePasswordForm(_check_password)
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_api_client.update_password(current_user.id, password=form.new_password.data)
|
||||
return redirect(url_for('.user_profile'))
|
||||
user_api_client.update_password(
|
||||
current_user.id, password=form.new_password.data
|
||||
)
|
||||
return redirect(url_for(".user_profile"))
|
||||
|
||||
return render_template(
|
||||
'views/user-profile/change-password.html',
|
||||
form=form
|
||||
)
|
||||
return render_template("views/user-profile/change-password.html", form=form)
|
||||
|
||||
|
||||
@main.route("/user-profile/disable-platform-admin-view", methods=['GET', 'POST'])
|
||||
@main.route("/user-profile/disable-platform-admin-view", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_disable_platform_admin_view():
|
||||
if not current_user.platform_admin and not session.get('disable_platform_admin_view'):
|
||||
if not current_user.platform_admin and not session.get(
|
||||
"disable_platform_admin_view"
|
||||
):
|
||||
abort(403)
|
||||
|
||||
form = ServiceOnOffSettingForm(
|
||||
name="Use platform admin view",
|
||||
enabled=not session.get('disable_platform_admin_view'),
|
||||
truthy='Yes',
|
||||
falsey='No',
|
||||
enabled=not session.get("disable_platform_admin_view"),
|
||||
truthy="Yes",
|
||||
falsey="No",
|
||||
)
|
||||
|
||||
form.enabled.param_extensions = {"hint": {"text": "Signing in again clears this setting"}}
|
||||
form.enabled.param_extensions = {
|
||||
"hint": {"text": "Signing in again clears this setting"}
|
||||
}
|
||||
|
||||
if form.validate_on_submit():
|
||||
session['disable_platform_admin_view'] = not form.enabled.data
|
||||
return redirect(url_for('.user_profile'))
|
||||
session["disable_platform_admin_view"] = not form.enabled.data
|
||||
return redirect(url_for(".user_profile"))
|
||||
|
||||
return render_template(
|
||||
'views/user-profile/disable-platform-admin-view.html',
|
||||
form=form
|
||||
"views/user-profile/disable-platform-admin-view.html", form=form
|
||||
)
|
||||
|
||||
@@ -11,80 +11,82 @@ from app.models.user import InvitedOrgUser, InvitedUser, User
|
||||
from app.utils.login import redirect_to_sign_in
|
||||
|
||||
|
||||
@main.route('/verify', methods=['GET', 'POST'])
|
||||
@main.route("/verify", methods=["GET", "POST"])
|
||||
@redirect_to_sign_in
|
||||
def verify():
|
||||
user_id = session['user_details']['id']
|
||||
user_id = session["user_details"]["id"]
|
||||
|
||||
def _check_code(code):
|
||||
return user_api_client.check_verify_code(user_id, code, 'sms')
|
||||
return user_api_client.check_verify_code(user_id, code, "sms")
|
||||
|
||||
form = TwoFactorForm(_check_code)
|
||||
|
||||
if form.validate_on_submit():
|
||||
session.pop('user_details', None)
|
||||
session.pop("user_details", None)
|
||||
return activate_user(user_id)
|
||||
|
||||
return render_template('views/two-factor-sms.html', form=form)
|
||||
return render_template("views/two-factor-sms.html", form=form)
|
||||
|
||||
|
||||
@main.route('/verify-email/<token>')
|
||||
@main.route("/verify-email/<token>")
|
||||
def verify_email(token):
|
||||
try:
|
||||
token_data = check_token(
|
||||
token,
|
||||
current_app.config['SECRET_KEY'],
|
||||
current_app.config['DANGEROUS_SALT'],
|
||||
current_app.config['EMAIL_EXPIRY_SECONDS']
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
current_app.config["EMAIL_EXPIRY_SECONDS"],
|
||||
)
|
||||
except SignatureExpired:
|
||||
flash("The link in the email we sent you has expired. We've sent you a new one.")
|
||||
return redirect(url_for('main.resend_email_verification'))
|
||||
flash(
|
||||
"The link in the email we sent you has expired. We've sent you a new one."
|
||||
)
|
||||
return redirect(url_for("main.resend_email_verification"))
|
||||
|
||||
# token contains json blob of format: {'user_id': '...', 'secret_code': '...'} (secret_code is unused)
|
||||
token_data = json.loads(token_data)
|
||||
user = User.from_id(token_data['user_id'])
|
||||
user = User.from_id(token_data["user_id"])
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if user.is_active:
|
||||
flash("That verification link has expired.")
|
||||
return redirect(url_for('main.sign_in'))
|
||||
return redirect(url_for("main.sign_in"))
|
||||
|
||||
if user.email_auth:
|
||||
session.pop('user_details', None)
|
||||
session.pop("user_details", None)
|
||||
return activate_user(user.id)
|
||||
|
||||
user.send_verify_code()
|
||||
session['user_details'] = {"email": user.email_address, "id": user.id}
|
||||
return redirect(url_for('main.verify'))
|
||||
session["user_details"] = {"email": user.email_address, "id": user.id}
|
||||
return redirect(url_for("main.verify"))
|
||||
|
||||
|
||||
def activate_user(user_id):
|
||||
user = User.from_id(user_id)
|
||||
# the user will have a new current_session_id set by the API - store it in the cookie for future requests
|
||||
session['current_session_id'] = user.current_session_id
|
||||
organization_id = session.get('organization_id')
|
||||
session["current_session_id"] = user.current_session_id
|
||||
organization_id = session.get("organization_id")
|
||||
activated_user = user.activate()
|
||||
activated_user.login()
|
||||
|
||||
invited_user = InvitedUser.from_session()
|
||||
if invited_user:
|
||||
service_id = _add_invited_user_to_service(invited_user)
|
||||
return redirect(url_for('main.service_dashboard', service_id=service_id))
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
|
||||
invited_org_user = InvitedOrgUser.from_session()
|
||||
if invited_org_user:
|
||||
user_api_client.add_user_to_organization(invited_org_user.organization, user_id)
|
||||
|
||||
if organization_id:
|
||||
return redirect(url_for('main.organization_dashboard', org_id=organization_id))
|
||||
return redirect(url_for("main.organization_dashboard", org_id=organization_id))
|
||||
else:
|
||||
return redirect(url_for('main.add_service', first='first'))
|
||||
return redirect(url_for("main.add_service", first="first"))
|
||||
|
||||
|
||||
def _add_invited_user_to_service(invitation):
|
||||
user = User.from_id(session['user_id'])
|
||||
user = User.from_id(session["user_id"])
|
||||
service_id = invitation.service
|
||||
user.add_to_service(
|
||||
service_id,
|
||||
|
||||
Reference in New Issue
Block a user