Merge branch 'main' of https://github.com/GSA/notifications-admin into 975-dashboard-messages-sent

This commit is contained in:
Jonathan Bobel
2023-12-13 10:41:49 -05:00
16 changed files with 145 additions and 174 deletions

View File

@@ -150,8 +150,14 @@ def _csp(config):
"https://js-agent.newrelic.com",
"https://gov-bam.nr-data.net",
"https://www.googletagmanager.com",
"https://www.google-analytics.com",
"https://dap.digitalgov.gov",
],
"connect-src": [
"'self'",
"https://gov-bam.nr-data.net",
"https://www.google-analytics.com",
],
"connect-src": ["'self'", "https://gov-bam.nr-data.net"],
"style-src": ["'self'", asset_domain],
"img-src": ["'self'", asset_domain, logo_domain],
}

View File

@@ -1196,7 +1196,7 @@ class AdminServiceSMSAllowanceForm(StripWhitespaceForm):
class AdminServiceMessageLimitForm(StripWhitespaceForm):
message_limit = GovukIntegerField(
"Number of messages the service is allowed to send each day",
"Max number of messages the service has per send",
validators=[DataRequired(message="Cannot be empty")],
)

View File

@@ -21,7 +21,7 @@ from app.main.forms import (
SearchUsersForm,
)
from app.models.user import InvitedUser, User
from app.utils.user import is_gov_user, user_has_permissions, user_is_platform_admin
from app.utils.user import is_gov_user, user_has_permissions
from app.utils.user_permissions import permission_options
@@ -42,7 +42,7 @@ def manage_users(service_id):
@main.route(
"/services/<uuid:service_id>/users/invite/<uuid:user_id>", methods=["GET", "POST"]
)
@user_is_platform_admin
@user_has_permissions("manage_service")
def invite_user(service_id, user_id=None):
form_class = InviteUserForm
form = form_class(

View File

@@ -69,7 +69,7 @@
'table-field-right-aligned' if align == 'right' else
'table-field-center-aligned' if align == 'center' else
'table-field-left-aligned'
%}
%}
{% set border = '' if border else 'table-field-noborder' %}
{% set wrap = 'table-field-wrap-text' if wrap else '' %}
@@ -169,7 +169,7 @@
{% if notification.status|format_notification_status_as_url(notification.notification_type) %}
</a>
{% endif %}
<p class="status-hint margin-0 width-card-lg">
<p class="status-hint margin-0 width-card ">
{{ notification.status|format_notification_status_as_time(
notification.created_at|format_datetime_short,
(notification.updated_at or notification.created_at)|format_datetime_short
@@ -180,6 +180,42 @@
{% endif %}
{% endmacro %}
{% macro notification_carrier_field(notification) %}
<!-- {% set displayed_on_single_line = notification.status in ['created', 'pending', 'sending', 'delivered', 'accepted', 'received'] %} -->
{% if not notification %}
{% call field(align='right') %}{% endcall %}
{% else %}
<!-- {% set status = notification.status|format_notification_status_as_field_status(notification.notification_type) %} -->
{% call field(
align='right'
) %}
<p class="status-hint margin-0 width-card">
{{ notification.carrier}}
</p>
{% if displayed_on_single_line %}</span>{% endif %}
{% endcall %}
{% endif %}
{% endmacro %}
{% macro notification_carrier_message_field(notification) %}
{% if not notification %}
{% call field(align='right') %}{% endcall %}
{% else %}
<!-- {% set status = notification.status|format_notification_status_as_field_status(notification.notification_type) %} -->
{% call field(
status=status,
align='right'
) %}
<p class="status-hint margin-0 width-card">
{{notification.provider_response}}
</p>
{% if displayed_on_single_line %}</span>{% endif %}
{% endcall %}
{% endif %}
{% endmacro %}
{% macro spark_bar_field(
count,
@@ -189,7 +225,7 @@
{% call field(align='right') %}
<span {% if id %}id="{{ id }}"{% endif %} class="spark-bar">
<span class="spark-bar-bar">
{{ '{:,.0f}'.format(count) }}
{{ '{:,.0f}'.format(count) }}
<!-- {% if count == 1 -%}
message sent
{% else -%}

View File

@@ -1,6 +1,6 @@
{% from "components/page-footer.html" import page_footer %}
{% from "components/previous-next-navigation.html" import previous_next_navigation %}
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading, hidden_field_heading, row_heading, notification_status_field %}
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading, hidden_field_heading, row_heading, notification_status_field, notification_carrier_field, notification_carrier_message_field %}
<div class="ajax-block-container" id='pill-selected-item'>
@@ -12,17 +12,19 @@
caption="Recent activity",
caption_visible=False,
empty_message='No messages found &thinsp;(messages are kept for {} days)'.format(limit_days)|safe,
field_headings=['Recipient', 'Status'],
field_headings=['Recipient', 'Status', 'Carrier', 'Carrier Response'],
field_headings_visible=False
) %}
{% call row_heading() %}
<a class="usa-link file-list-filename" href="{{ single_notification_url(notification_id=item.id) }}">{{ item.to.splitlines()|join(', ') if item.to else '' }}</a>
<p class="file-list-hint">
<p class="file-list-hint width-card">
{{ item.preview_of_content }}
</p>
{% endcall %}
{{ notification_status_field(item) }}
{{ notification_carrier_field(item) }}
{{ notification_carrier_message_field(item)}}
{% endcall %}
{% if notifications %}

View File

@@ -9,9 +9,19 @@
{% block maincolumn_content %}
<h1 class="font-body-lg">
Team members
</h1>
<div class="display-flex flex-justify flex-align-center">
<h1 class="font-body-xl margin-0">
Team members
</h1>
{% if current_user.has_permissions('manage_service') %}
{{ usaButton({
"element": "a",
"text": "Invite a team member",
"href": url_for('.invite_user', service_id=current_service.id),
"classes": "usa-button--outline"
}) }}
{% endif %}
</div>
{% if show_search_box %}
<div data-module="autofocus">
@@ -84,15 +94,4 @@
{% endfor %}
</div>
{% if current_user.has_permissions('manage_service') %}
<div class="js-stick-at-bottom-when-scrolling">
{{ usaButton({
"element": "a",
"text": "Invite a team member",
"href": url_for('.invite_user', service_id=current_service.id),
"classes": "usa-button"
}) }}
</div>
{% endif %}
{% endblock %}

View File

@@ -294,9 +294,9 @@
{{ edit_field('Change', url_for('.set_rate_limit', service_id=current_service.id), suffix='rate limit') }}
{% endcall %}
{% call row() %}
{{ text_field('Message limit')}}
{{ text_field('{:,} per day'.format(current_service.message_limit)) }}
{{ edit_field('Change', url_for('.set_message_limit', service_id=current_service.id), suffix='daily message limit') }}
{{ text_field('Message batch limit')}}
{{ text_field('{:,} per send'.format(current_service.message_limit)) }}
{{ edit_field('Change', url_for('.set_message_limit', service_id=current_service.id), suffix='message batch limit') }}
{% endcall %}
{% call row() %}
{{ text_field('Free text message allowance')}}

View File

@@ -5,7 +5,7 @@
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block service_page_title %}
Daily message limit
Message batch limit
{% endblock %}
{% block backLink %}
@@ -15,7 +15,7 @@
{% block maincolumn_content %}
{% call form_wrapper() %}
{{ page_header('Daily message limit') }}
{{ page_header('Message batch limit') }}
{{ form.message_limit }}
{{ page_footer('Save') }}
{% endcall %}

View File

@@ -16,9 +16,8 @@ Notify.gov
<div class="grid-container padding-y-4">
<div class="grid-row grid-gap display-flex flex-align-center">
<div class="desktop:grid-col-7 tablet:grid-col-12">
<h1 class="font-serif-2xl usa-hero__heading">Send text messages to your participants</h1>
<p class="font-sans-lg">Notify.gov is a text messaging service that helps federal, state, local, tribal, and territorial governments more
effectively communicate with their program participants.</p>
<h1 class="font-serif-2xl usa-hero__heading">Reach people where they are with government-powered text messages</h1>
<p class="font-sans-lg">Notify.gov is a text message service that helps federal, state, local, tribal and territorial governments more effectively communicate with the people they serve.</p>
<div class="usa-button-group margin-bottom-5">
<a class="usa-button usa-button--big margin-right-2" href="{{ url_for('main.sign_in' )}}">Sign in</a>
if you are an existing pilot partner
@@ -304,4 +303,4 @@ Notify.gov
</div>-->
{% endblock %}
{% endblock %}

View File

@@ -38,18 +38,12 @@
{% for item in template_list %}
{% set item_link_content %}
{% for ancestor in item.ancestors %}
<a href="{{ url_for('.choose_template', service_id=current_service.id, template_type=template_type, template_folder_id=ancestor.id) }}"
class="usa-link template-list-folder">
{{- format_item_name(ancestor.name) -}}
</a> <span class="message-name-separator"></span>
{% endfor %}
{% if item.is_folder %}
<a href="{{ url_for('.choose_template', service_id=current_service.id, template_type=template_type, template_folder_id=item.id) }}"
class="usa-link template-list-folder">
<span class="live-search-relevant">{{- format_item_name(item.name) -}}</span>
</a>
{% else %}
{% elif not item.ancestors %}
<a href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.id) }}"
class="usa-link template-list-template">
<span class="live-search-relevant">
@@ -70,9 +64,11 @@
{% endset %}
{% set item_meta %}
{% if not item.ancestors %}
<span id="{{ item.id }}-item-hint" class="usa-hint usa-checkbox__label-description template-list-item-hint">
{{ item.hint }}
</span>
{% endif %}
{% endset %}
{# create the item config now to include the label content -#}
@@ -112,4 +108,4 @@
}) }}
{% endif %}
</nav>
{% endif %}
{% endif %}

View File

@@ -81,7 +81,7 @@ def _mock_get_service_settings_page_common(
"Notes None Change the notes for the service",
"Organization Test organization Federal government Change organization for service",
"Rate limit 3,000 per minute Change rate limit",
"Message limit 1,000 per day Change daily message limit",
"Message batch limit 1,000 per send Change message batch limit",
"Free text message allowance 250,000 per year Change free text message allowance",
"Email branding GOV.UK Change email branding (admin view)",
"Custom data retention Email 7 days Change data retention",
@@ -3171,7 +3171,7 @@ def test_should_show_page_to_set_message_limit(
client_request.login(platform_admin_user)
page = client_request.get("main.set_message_limit", service_id=SERVICE_ONE_ID)
assert normalize_spaces(page.select_one("label").text) == (
"Number of messages the service is allowed to send each day"
"Max number of messages the service has per send"
)
assert normalize_spaces(page.select_one("input[type=text]")["value"]) == ("1000")

View File

@@ -18,9 +18,15 @@ def test_owasp_useful_headers_set(
assert search(
r"script-src 'self' static\.example\.com 'unsafe-eval' https:\/\/js-agent\.new"
r"relic\.com https:\/\/gov-bam\.nr-data\.net https:\/\/www\.googletagmanager\."
r"com 'nonce-.*';",
r"com https:\/\/www\.google-analytics\."
r"com https:\/\/dap\.digitalgov\."
r"gov 'nonce-.*';",
csp,
)
assert search(
r"connect-src 'self' https:\/\/gov-bam.nr-data\.net https:\/\/www\.google-analytics\."
r"com;",
csp,
)
assert search(r"connect-src 'self' https:\/\/gov-bam.nr-data\.net;", csp)
assert search(r"style-src 'self' static\.example\.com 'nonce-.*';", csp)
assert search(r"img-src 'self' static\.example\.com static-logos\.test\.com", csp)

View File

@@ -15,7 +15,7 @@ def test_non_logged_in_user_can_see_homepage(
client_request.logout()
page = client_request.get("main.index", _test_page_title=False)
assert page.h1.text.strip() == ("Send text messages to your participants")
assert page.h1.text.strip() == ("Reach people where they are with government-powered text messages")
assert page.select_one("a.usa-button.usa-button--big")["href"] == url_for(
"main.sign_in",

View File

@@ -762,9 +762,9 @@ def test_edit_user_permissions_shows_authentication_for_email_auth_service(
def test_should_show_page_for_inviting_user(
client_request,
mock_get_template_folders,
platform_admin_user,
active_user_with_permissions,
):
client_request.login(platform_admin_user)
client_request.login(active_user_with_permissions)
page = client_request.get(
"main.invite_user",
service_id=SERVICE_ONE_ID,
@@ -774,6 +774,17 @@ def test_should_show_page_for_inviting_user(
assert not page.find("div", class_="checkboxes-nested")
def test_should_not_show_page_for_inviting_user_without_permissions(
client_request, mock_get_template_folders, active_user_empty_permissions
):
client_request.login(active_user_empty_permissions)
page = client_request.get(
"main.invite_user", service_id=SERVICE_ONE_ID, _expected_status=403
)
assert "not allowed to see this page" in page.h1.string.strip()
def test_should_show_page_for_inviting_user_with_email_prefilled(
client_request,
mocker,
@@ -801,9 +812,12 @@ def test_should_show_page_for_inviting_user_with_email_prefilled(
# We have the users name in the H1 but dont want it duplicated
# in the page title
_test_page_title=False,
_expected_status=403,
)
assert "not allowed to see this page" in page.h1.string.strip()
assert normalize_spaces(page.select_one("title").text).startswith(
"Invite a team member"
)
assert normalize_spaces(page.select_one("h1").text) == ("Invite Service Two User")
assert not page.select("input#email_address") or page.select("input[type=email]")
def test_should_show_page_if_prefilled_user_is_already_a_team_member(
@@ -813,9 +827,8 @@ def test_should_show_page_if_prefilled_user_is_already_a_team_member(
fake_uuid,
active_user_with_permissions,
active_caseworking_user,
platform_admin_user,
):
client_request.login(platform_admin_user)
client_request.login(active_user_with_permissions)
mocker.patch(
"app.models.user.user_api_client.get_user",
side_effect=[
@@ -1280,9 +1293,11 @@ def test_user_cant_invite_themselves(
"permissions_field": ["send_messages", "manage_service", "manage_api_keys"],
},
_follow_redirects=True,
_expected_status=403,
_expected_status=200,
)
assert "not allowed to see this page" in page.h1.string.strip()
assert page.h1.string.strip() == "Invite a team member"
form_error = page.find("span", class_="usa-error-message").text.strip()
assert form_error == "Error: You cannot send an invitation to yourself"
assert not mock_create_invite.called

View File

@@ -58,18 +58,10 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
["Email", "Text message"],
[
"folder_one folder_one 2 folders",
("folder_one folder_one_one " "folder_one folder_one_one " "1 folder"),
(
"folder_one folder_one_one folder_one_one_one "
"folder_one folder_one_one folder_one_one_one "
"1 template"
),
(
"folder_one folder_one_one folder_one_one_one sms_template_nested "
"folder_one folder_one_one folder_one_one_one sms_template_nested "
"Text message template"
),
("folder_one folder_one_two " "folder_one folder_one_two " "Empty"),
("folder_one folder_one_one " "folder_one_one"),
("folder_one folder_one_one folder_one_one_one " "folder_one_one_one"),
("folder_one folder_one_one folder_one_one_one sms_template_nested"),
("folder_one folder_one_two " "folder_one_two"),
"folder_two folder_two Empty",
("sms_template_one " "sms_template_one " "Text message template"),
("sms_template_two " "sms_template_two " "Text message template"),
@@ -88,7 +80,6 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
"folder_one",
"folder_one_one",
"folder_one_one_one",
"sms_template_nested",
"folder_one_two",
"folder_two",
"sms_template_one",
@@ -106,18 +97,10 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
["Email", "Text message"],
[
"folder_one folder_one 2 folders",
("folder_one folder_one_one " "folder_one folder_one_one " "1 folder"),
(
"folder_one folder_one_one folder_one_one_one "
"folder_one folder_one_one folder_one_one_one "
"1 template"
),
(
"folder_one folder_one_one folder_one_one_one sms_template_nested "
"folder_one folder_one_one folder_one_one_one sms_template_nested "
"Text message template"
),
("folder_one folder_one_two " "folder_one folder_one_two " "Empty"),
("folder_one folder_one_one " "folder_one_one"),
("folder_one folder_one_one folder_one_one_one " "folder_one_one_one"),
("folder_one folder_one_one folder_one_one_one sms_template_nested"),
("folder_one folder_one_two " "folder_one_two"),
"folder_two folder_two Empty",
"sms_template_one sms_template_one Text message template",
"sms_template_two sms_template_two Text message template",
@@ -136,7 +119,6 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
"folder_one",
"folder_one_one",
"folder_one_one_one",
"sms_template_nested",
"folder_one_two",
"folder_two",
"sms_template_one",
@@ -154,17 +136,9 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
["All", "Email"],
[
"folder_one folder_one 1 folder",
("folder_one folder_one_one " "folder_one folder_one_one " "1 folder"),
(
"folder_one folder_one_one folder_one_one_one "
"folder_one folder_one_one folder_one_one_one "
"1 template"
),
(
"folder_one folder_one_one folder_one_one_one sms_template_nested "
"folder_one folder_one_one folder_one_one_one sms_template_nested "
"Text message template"
),
("folder_one folder_one_one " "folder_one_one"),
("folder_one folder_one_one folder_one_one_one " "folder_one_one_one"),
("folder_one folder_one_one folder_one_one_one sms_template_nested"),
"sms_template_one sms_template_one Text message template",
"sms_template_two sms_template_two Text message template",
],
@@ -177,7 +151,6 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
"folder_one",
"folder_one_one",
"folder_one_one_one",
"sms_template_nested",
"sms_template_one",
"sms_template_two",
],
@@ -191,16 +164,8 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
["Email", "Text message"],
[
"folder_one_one folder_one_one 1 folder",
(
"folder_one_one folder_one_one_one "
"folder_one_one folder_one_one_one "
"1 template"
),
(
"folder_one_one folder_one_one_one sms_template_nested "
"folder_one_one folder_one_one_one sms_template_nested "
"Text message template"
),
("folder_one_one folder_one_one_one " "folder_one_one_one"),
("folder_one_one folder_one_one_one sms_template_nested"),
"folder_one_two folder_one_two Empty",
],
[
@@ -210,7 +175,6 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
[
"folder_one_one",
"folder_one_one_one",
"sms_template_nested",
"folder_one_two",
],
None,
@@ -223,25 +187,13 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
["All", "Email"],
[
"folder_one_one folder_one_one 1 folder",
(
"folder_one_one folder_one_one_one "
"folder_one_one folder_one_one_one "
"1 template"
),
(
"folder_one_one folder_one_one_one sms_template_nested "
"folder_one_one folder_one_one_one sms_template_nested "
"Text message template"
),
("folder_one_one folder_one_one_one " "folder_one_one_one"),
("folder_one_one folder_one_one_one sms_template_nested"),
],
[
"folder_one_one folder_one_one 1 folder",
],
[
"folder_one_one",
"folder_one_one_one",
"sms_template_nested",
],
["folder_one_one", "folder_one_one_one"],
None,
),
(
@@ -266,19 +218,12 @@ def _folder(name, folder_id=None, parent=None, users_with_permission=None):
["Email", "Text message"],
[
"folder_one_one_one folder_one_one_one 1 template",
(
"folder_one_one_one sms_template_nested "
"folder_one_one_one sms_template_nested "
"Text message template"
),
("folder_one_one_one sms_template_nested"),
],
[
"folder_one_one_one folder_one_one_one 1 template",
],
[
"folder_one_one_one",
"sms_template_nested",
],
["folder_one_one_one"],
None,
),
(
@@ -502,7 +447,6 @@ def test_template_id_is_searchable_for_services_with_api_keys(
)
] == [
"folder one",
f'{template_2["id"]} template two',
f'{template_1["id"]} template one',
]
@@ -523,10 +467,7 @@ def test_template_id_is_searchable_for_services_with_api_keys(
# Text which should be hidden from all users
r".template-list-item .display-none"
)
] == [
template_2["id"],
template_1["id"],
]
] == [template_1["id"]]
mock_get_api_keys.assert_called_once_with(SERVICE_ONE_ID)
@@ -1707,21 +1648,10 @@ def test_show_custom_error_message(
],
[
["folder_A", "folder_A", "1 template, 2 folders"],
["folder_A folder_C", "folder_A", "folder_C", "1 template"],
[
"folder_A folder_C sms_template_C",
"folder_A",
"folder_C",
"sms_template_C",
"Text message template",
],
["folder_A folder_D", "folder_A", "folder_D", "Empty"],
[
"folder_A sms_template_A",
"folder_A",
"sms_template_A",
"Text message template",
],
["folder_A folder_C", "folder_C"],
["folder_A folder_C sms_template_C"],
["folder_A folder_D", "folder_D"],
["folder_A sms_template_A"],
[
"folder_E folder_F folder_G",
"folder_E",
@@ -1729,14 +1659,7 @@ def test_show_custom_error_message(
"folder_G",
"1 template",
],
[
"folder_E folder_F folder_G email_template_G",
"folder_E",
"folder_F",
"folder_G",
"email_template_G",
"Email template",
],
["folder_E folder_F folder_G email_template_G"],
["email_template_root", "email_template_root", "Email template"],
],
None,
@@ -1763,11 +1686,6 @@ def test_show_custom_error_message(
],
[
"folder_E folder_F folder_G email_template_G",
"folder_E",
"folder_F",
"folder_G",
"email_template_G",
"Email template",
],
["email_template_root", "email_template_root", "Email template"],
],
@@ -1780,20 +1698,9 @@ def test_show_custom_error_message(
],
[
["folder_A", "folder_A", "1 template, 1 folder"],
["folder_A folder_C", "folder_A", "folder_C", "1 template"],
[
"folder_A folder_C sms_template_C",
"folder_A",
"folder_C",
"sms_template_C",
"Text message template",
],
[
"folder_A sms_template_A",
"folder_A",
"sms_template_A",
"Text message template",
],
["folder_A folder_C", "folder_C"],
["folder_A folder_C sms_template_C"],
["folder_A sms_template_A"],
],
None,
),

View File

@@ -1067,6 +1067,11 @@ def active_user_with_permissions(fake_uuid):
return create_active_user_with_permissions()
@pytest.fixture()
def active_user_empty_permissions(fake_uuid):
return create_active_user_empty_permissions()
@pytest.fixture()
def active_user_with_permission_to_two_services(fake_uuid):
permissions = [