notify-api-412 use black to enforce python coding style

This commit is contained in:
Kenneth Kehl
2023-08-25 09:12:23 -07:00
parent c6eb007386
commit 8c9721d8e2
201 changed files with 31660 additions and 28105 deletions

View File

@@ -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, Im trying out Notify.gov. Today is ((day of week)) and my favorite color is ((color)).',
"Example text message template",
"sms",
"Hi, Im 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,

View File

@@ -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,
)

View File

@@ -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"))

View File

@@ -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))

View File

@@ -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"],
}

View File

@@ -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))
)

View File

@@ -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,
)

View File

@@ -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):

View File

@@ -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,
)

View File

@@ -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 cant 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 cant 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,
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 industrys 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 industrys 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)

View File

@@ -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(
"""
Youre 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(
"""
Youre 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"))

View File

@@ -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
)

View File

@@ -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))

View File

@@ -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
)

View File

@@ -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 youve just sent a notification. We
# only want to show the back link when youve navigated to a
# notification, not when youve 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,
)
},
)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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 hasnt 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

View File

@@ -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(),
)

View File

@@ -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"]
)

View File

@@ -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")

View File

@@ -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

View File

@@ -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"&ensp;<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"&ensp;<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))

View File

@@ -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"))

View File

@@ -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

View File

@@ -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))

View File

@@ -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
)

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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,