mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-23 07:46:23 -04:00
Merge branch 'main' of https://github.com/GSA/notifications-admin into e2e-test-invite-to-service
This commit is contained in:
2
Makefile
2
Makefile
@@ -82,7 +82,7 @@ dead-code:
|
|||||||
.PHONY: e2e-test
|
.PHONY: e2e-test
|
||||||
e2e-test: export NEW_RELIC_ENVIRONMENT=test
|
e2e-test: export NEW_RELIC_ENVIRONMENT=test
|
||||||
e2e-test: ## Run end-to-end integration tests; note that --browser webkit isn't currently working
|
e2e-test: ## Run end-to-end integration tests; note that --browser webkit isn't currently working
|
||||||
poetry run pytest -v --browser chromium --browser firefox tests/end_to_end
|
poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
|
||||||
|
|
||||||
.PHONY: js-lint
|
.PHONY: js-lint
|
||||||
js-lint: ## Run javascript linting scanners
|
js-lint: ## Run javascript linting scanners
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from app.formatters import (
|
|||||||
format_datetime_human,
|
format_datetime_human,
|
||||||
format_datetime_normal,
|
format_datetime_normal,
|
||||||
format_datetime_relative,
|
format_datetime_relative,
|
||||||
|
format_datetime_scheduled_notification,
|
||||||
format_datetime_table,
|
format_datetime_table,
|
||||||
format_day_of_week,
|
format_day_of_week,
|
||||||
format_delta,
|
format_delta,
|
||||||
@@ -551,6 +552,7 @@ def add_template_filters(application):
|
|||||||
format_datetime,
|
format_datetime,
|
||||||
format_datetime_24h,
|
format_datetime_24h,
|
||||||
format_datetime_normal,
|
format_datetime_normal,
|
||||||
|
format_datetime_scheduled_notification,
|
||||||
format_datetime_table,
|
format_datetime_table,
|
||||||
valid_phone_number,
|
valid_phone_number,
|
||||||
linkable_name,
|
linkable_name,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from notifications_utils.recipients import InvalidPhoneError, validate_phone_num
|
|||||||
from notifications_utils.take import Take
|
from notifications_utils.take import Take
|
||||||
|
|
||||||
from app.utils.csv import get_user_preferred_timezone
|
from app.utils.csv import get_user_preferred_timezone
|
||||||
from app.utils.time import parse_dt, parse_naive_dt
|
from app.utils.time import parse_naive_dt
|
||||||
|
|
||||||
|
|
||||||
def apply_html_class(tags, html_file):
|
def apply_html_class(tags, html_file):
|
||||||
@@ -93,16 +93,34 @@ def format_datetime_normal(date):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_datetime_scheduled_notification(date):
|
||||||
|
# e.g. April 09, 2024 at 04:00 PM US/Eastern.
|
||||||
|
# Everything except scheduled notifications, the time is always "now".
|
||||||
|
# Scheduled notifications are the exception to the rule.
|
||||||
|
# Here we are formating and displaying the datetime without converting datetime to a different timezone.
|
||||||
|
|
||||||
|
datetime_obj = parse_naive_dt(date)
|
||||||
|
|
||||||
|
format_time_without_tz = datetime_obj.replace(tzinfo=timezone.utc).strftime(
|
||||||
|
"%I:%M %p"
|
||||||
|
)
|
||||||
|
return "{} at {} {}".format(
|
||||||
|
format_date_normal(date), format_time_without_tz, get_user_preferred_timezone()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_datetime_table(date):
|
def format_datetime_table(date):
|
||||||
# example: 03-18-2024 at 04:53 PM, intended for datetimes in tables
|
# example: 03-18-2024 at 04:53 PM, intended for datetimes in tables
|
||||||
return "{} at {}".format(format_date_numeric(date), format_time_12h(date))
|
return "{} at {}".format(format_date_numeric(date), format_time_12h(date))
|
||||||
|
|
||||||
|
|
||||||
def format_time_12h(date):
|
def format_time_12h(date):
|
||||||
date = parse_dt(date)
|
date = parse_naive_dt(date)
|
||||||
|
|
||||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||||
return date.astimezone(preferred_tz).strftime("%I:%M %p")
|
return (
|
||||||
|
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_datetime_relative(date):
|
def format_datetime_relative(date):
|
||||||
|
|||||||
@@ -662,7 +662,21 @@ def create_global_stats(services):
|
|||||||
"email": {"delivered": 0, "failed": 0, "requested": 0},
|
"email": {"delivered": 0, "failed": 0, "requested": 0},
|
||||||
"sms": {"delivered": 0, "failed": 0, "requested": 0},
|
"sms": {"delivered": 0, "failed": 0, "requested": 0},
|
||||||
}
|
}
|
||||||
|
# Issue #1323. The back end is now sending 'failure' instead of
|
||||||
|
# 'failed'. Adjust it here, but keep it flexible in case
|
||||||
|
# the backend reverts to 'failed'.
|
||||||
for service in services:
|
for service in services:
|
||||||
|
if service["statistics"]["sms"].get("failure") is not None:
|
||||||
|
service["statistics"]["sms"]["failed"] = service["statistics"]["sms"][
|
||||||
|
"failure"
|
||||||
|
]
|
||||||
|
if service["statistics"]["email"].get("failure") is not None:
|
||||||
|
service["statistics"]["email"]["failed"] = service["statistics"]["email"][
|
||||||
|
"failure"
|
||||||
|
]
|
||||||
|
|
||||||
|
for service in services:
|
||||||
|
|
||||||
for msg_type, status in itertools.product(
|
for msg_type, status in itertools.product(
|
||||||
("sms", "email"), ("delivered", "failed", "requested")
|
("sms", "email"), ("delivered", "failed", "requested")
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -650,7 +650,9 @@ def check_messages(service_id, template_id, upload_id, row_index=2):
|
|||||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||||
def preview_job(service_id, template_id, upload_id, row_index=2):
|
def preview_job(service_id, template_id, upload_id, row_index=2):
|
||||||
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
||||||
data = _check_messages(service_id, template_id, upload_id, row_index, force_hide_sender=True)
|
data = _check_messages(
|
||||||
|
service_id, template_id, upload_id, row_index, force_hide_sender=True
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/check/preview.html",
|
"views/check/preview.html",
|
||||||
@@ -911,7 +913,9 @@ def preview_notification(service_id, template_id):
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/notifications/preview.html",
|
"views/notifications/preview.html",
|
||||||
**_check_notification(service_id, template_id, show_recipient=False, force_hide_sender=True),
|
**_check_notification(
|
||||||
|
service_id, template_id, show_recipient=False, force_hide_sender=True
|
||||||
|
),
|
||||||
scheduled_for=session["scheduled_for"],
|
scheduled_for=session["scheduled_for"],
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ def _get_access_token(code, state):
|
|||||||
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
||||||
headers = {"Authorization": "Bearer %s" % token}
|
headers = {"Authorization": "Bearer %s" % token}
|
||||||
response = requests.post(url, headers=headers)
|
response = requests.post(url, headers=headers)
|
||||||
|
if response.json().get("access_token") is None:
|
||||||
|
# Capture the response json here so it hopefully shows up in error reports
|
||||||
|
current_app.logger.error(f"Error when getting access token {response.json()}")
|
||||||
|
raise KeyError(f"'access_token' {response.json()}")
|
||||||
access_token = response.json()["access_token"]
|
access_token = response.json()["access_token"]
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
@@ -84,13 +88,17 @@ def _do_login_dot_gov():
|
|||||||
code = request.args.get("code")
|
code = request.args.get("code")
|
||||||
state = request.args.get("state")
|
state = request.args.get("state")
|
||||||
login_gov_error = request.args.get("error")
|
login_gov_error = request.args.get("error")
|
||||||
if code and state:
|
|
||||||
access_token = _get_access_token(code, state)
|
if login_gov_error:
|
||||||
user_email, user_uuid = _get_user_email_and_uuid(access_token)
|
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
||||||
redirect_url = request.args.get("next")
|
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||||
|
elif code and state:
|
||||||
|
|
||||||
# activate the user
|
# activate the user
|
||||||
try:
|
try:
|
||||||
|
access_token = _get_access_token(code, state)
|
||||||
|
user_email, user_uuid = _get_user_email_and_uuid(access_token)
|
||||||
|
redirect_url = request.args.get("next")
|
||||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||||
activate_user(user["id"])
|
activate_user(user["id"])
|
||||||
except BaseException as be: # noqa B036
|
except BaseException as be: # noqa B036
|
||||||
@@ -99,9 +107,6 @@ def _do_login_dot_gov():
|
|||||||
|
|
||||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||||
|
|
||||||
elif login_gov_error:
|
|
||||||
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
|
||||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
|
||||||
# end login.gov
|
# end login.gov
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
30
app/templates/new/components/main_nav.html
Normal file
30
app/templates/new/components/main_nav.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% if help %}
|
||||||
|
{% include 'partials/tour.html' %}
|
||||||
|
{% else %}
|
||||||
|
<nav class="nav margin-bottom-4">
|
||||||
|
<a class="usa-button margin-top-1 margin-bottom-5 width-full"
|
||||||
|
href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a>
|
||||||
|
<ul class="usa-sidenav">
|
||||||
|
{% if current_user.has_permissions() %}
|
||||||
|
{% if current_user.has_permissions('view_activity') %}
|
||||||
|
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if not current_user.has_permissions('view_activity') %}
|
||||||
|
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||||
|
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
|
||||||
|
{% endif %}
|
||||||
|
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||||
|
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||||
|
{% endif %} -->
|
||||||
|
{% if current_user.has_permissions('manage_api_keys') %}
|
||||||
|
<!-- <li><a class="usa-link{{ main_navigation.is_selected('api-integration') }}" href="{{ url_for('.api_integration', service_id=current_service.id) }}">API integration</a></li> -->
|
||||||
|
{% endif %}
|
||||||
|
{% elif current_user.has_permissions(allow_org_user=True) %}
|
||||||
|
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||||
|
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
11
app/templates/new/components/org_nav.html
Normal file
11
app/templates/new/components/org_nav.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<nav class="navigation">
|
||||||
|
<ul>
|
||||||
|
<li><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Usage</a></li>
|
||||||
|
<li><a class="usa-link{{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
|
||||||
|
{% if current_user.platform_admin %}
|
||||||
|
<li><a class="usa-link{{ org_navigation.is_selected('settings') }}" href="{{ url_for('.organization_settings', org_id=current_org.id) }}">Settings</a></li>
|
||||||
|
<li><a class="usa-link{{ org_navigation.is_selected('trial-services') }}" href="{{ url_for('.organization_trial_mode_services', org_id=current_org.id) }}">Trial mode services</a></li>
|
||||||
|
<li><a class="usa-link{{ org_navigation.is_selected('billing') }}" href="{{ url_for('.organization_billing', org_id=current_org.id) }}">Billing</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
15
app/templates/new/components/service_navigation.html
Normal file
15
app/templates/new/components/service_navigation.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="navigation-service margin-top-5 display-flex flex-align-end flex-justify border-bottom padding-bottom-1">
|
||||||
|
{% if current_service.organization_id %}
|
||||||
|
{% if current_user.platform_admin or
|
||||||
|
(current_user.belongs_to_organization(current_service.organization_id) and current_service.live) %}
|
||||||
|
<a href="{{ url_for('.organization_dashboard', org_id=current_service.organization_id) }}" class="usa-link navigation-organization-link">{{ current_service.organization_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="font-body-2xl text-bold">
|
||||||
|
{{ current_service.name }}
|
||||||
|
{% if not current_service.active %}
|
||||||
|
<span class="navigation-service-name navigation-service-type--suspended">Suspended</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('main.choose_account') }}" class="usa-link">Switch service</a>
|
||||||
|
</div>
|
||||||
16
app/templates/new/components/settings_navigation.html
Normal file
16
app/templates/new/components/settings_navigation.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% if help %}
|
||||||
|
{% include 'partials/tour.html' %}
|
||||||
|
{% else %}
|
||||||
|
<nav class="nav">
|
||||||
|
<ul class="usa-sidenav">
|
||||||
|
{# {% if current_user.has_permissions() %} #}
|
||||||
|
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}"
|
||||||
|
href="{{ url_for('main.service_settings', service_id=current_service.id) }}">General</a></li>
|
||||||
|
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('user-profile') }}"
|
||||||
|
href="{{ url_for('main.user_profile', service_id=current_service.id) }}">User profile</a></li>
|
||||||
|
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('team-members') }}"
|
||||||
|
href="{{ url_for('main.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||||
|
{# {% endif %} #}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "/new/base.html" %}
|
||||||
|
|
||||||
{% block per_page_title %}
|
{% block per_page_title %}
|
||||||
{% block org_page_title %}{% endblock %} – {{ current_org.name }}
|
{% block org_page_title %}{% endblock %} – {{ current_org.name }}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
<div class="grid-col-3">
|
<div class="grid-col-3">
|
||||||
{% include "org_nav.html" %}
|
{% include "/new/components/org_nav.html" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-col-9">
|
<div class="grid-col-9">
|
||||||
{% block beforeContent %}
|
{% block beforeContent %}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "/new/base.html" %}
|
||||||
|
|
||||||
{% block per_page_title %}
|
{% block per_page_title %}
|
||||||
{% block service_page_title %}{% endblock %} – {{ current_service.name }}
|
{% block service_page_title %}{% endblock %} – {{ current_service.name }}
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
{% block serviceNavigation %}
|
{% block serviceNavigation %}
|
||||||
{% include "service_navigation.html" %}
|
{% include "new/components/service_navigation.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<!-- The withnav_template can be used to replace the settings_template. when it comes to setting_template and withnav_template, this service_navigation.html is only used in withnav_template and in settings_template, it was not used. That is one out of the two differences between settings template and withnav template. Child templates that extends settings_template, include the block serviceNavigation but leave it empty. Within the app, the only pages settings_template.html is used: manage-users.html, service-settings.html, and user-profile.html -->
|
<!-- The withnav_template can be used to replace the settings_template. When it comes to setting_template and withnav_template, this service_navigation.html on line 10 is only used in withnav_template. In settings_template, it was not used. That is one out of the two differences between settings template and withnav template. Child templates that extends settings_template, include the block serviceNavigation but leave it empty because we don't need to call in service_navigation. Within the app, the only pages settings_template.html is used: manage-users.html, service-settings.html, and user-profile.html -->
|
||||||
<div class="grid-row margin-top-5">
|
<div class="grid-row margin-top-5">
|
||||||
{% if help %}
|
{% if help %}
|
||||||
<div class="grid-col-3">
|
<div class="grid-col-3">
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
<div class="grid-col-3">
|
<div class="grid-col-3">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block sideNavigation %}
|
{% block sideNavigation %}
|
||||||
{% include "main_nav.html" %}
|
{% include "/new/components/main_nav.html" %}
|
||||||
<!-- include settings_nav.html for child templates that used settings_template -->
|
<!-- instead of "main_nav.html" include "settings_nav.html" for child templates that extends settings_template -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% if help %}
|
{% if help %}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ This document serves as a glossary for the templates directory structure of the
|
|||||||
- `head.html`: Template for the site's <head>, included in `base.html`.
|
- `head.html`: Template for the site's <head>, included in `base.html`.
|
||||||
- `header.html`: Template for the site's header, included in `base.html`.
|
- `header.html`: Template for the site's header, included in `base.html`.
|
||||||
- `footer.html`: Template for the site's footer, included in `base.html`.
|
- `footer.html`: Template for the site's footer, included in `base.html`.
|
||||||
|
- `settings_navigation.html`: The settings navigation used in `withnav_template.html` that previously extended `settings_template.html`.
|
||||||
|
- `org_nav.html`: The organization's navigation used solely in `org_template.html`.
|
||||||
|
- `main_nav.html`: The main navigation used in `withnav_template.html`
|
||||||
|
- `service_navigation.html`: The service navigation used in `withnav_template.html`. In withnav_template.html, the `serviceNavigation` block will be left empty in any child templates that previously extended `settings_template.html`.
|
||||||
- **/views** (or **/pages**): Individual page templates that use the base layouts, components, and partials to present content.
|
- **/views** (or **/pages**): Individual page templates that use the base layouts, components, and partials to present content.
|
||||||
|
|
||||||
### Best Practices
|
### Best Practices
|
||||||
@@ -30,9 +34,9 @@ This document serves as a glossary for the templates directory structure of the
|
|||||||
- withoutnav_template.html Delete
|
- withoutnav_template.html Delete
|
||||||
- main_template.html Delete
|
- main_template.html Delete
|
||||||
- settings_templates.html `withnav_template` can be used to replace `settings_template`.
|
- settings_templates.html `withnav_template` can be used to replace `settings_template`.
|
||||||
- settings_nav.html (move to /new/navigation directory)
|
- settings_nav.html (move to /components/ directory)
|
||||||
- main_nav.html (move to /new/navigation directory)
|
- main_nav.html (move to /components/ directory)
|
||||||
- service_navigation.html (move to /new/navigation directory)
|
- service_navigation.html (move to /components/ directory)
|
||||||
- org_template, could be under it's own directory called /layout/organization
|
- org_template, could be under it's own directory called /layout/organization
|
||||||
- org_nav.html (move to /new/navigation directory)
|
- org_nav.html (move to /components/ directory)
|
||||||
- content_template.html Delete
|
- content_template.html Delete
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
{{ page_header('Preview') }}
|
{{ page_header('Preview') }}
|
||||||
<div>
|
<div>
|
||||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_normal if scheduled_for else 'Now'}}</p>
|
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_scheduled_notification if scheduled_for else 'Now'}}</p>
|
||||||
<p class="sms-message-file-name">File: {{original_file_name}}</p>
|
<p class="sms-message-file-name">File: {{original_file_name}}</p>
|
||||||
<p class="sms-message-template">Template: {{template.name}}</p>
|
<p class="sms-message-template">Template: {{template.name}}</p>
|
||||||
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
{{ page_header('Preview') }}
|
{{ page_header('Preview') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_normal if scheduled_for else 'Now'}}</p>
|
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_scheduled_notification if scheduled_for else 'Now'}}</p>
|
||||||
<p class="sms-message-template">Template: {{template.name}}</p>
|
<p class="sms-message-template">Template: {{template.name}}</p>
|
||||||
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
||||||
<p class="sms-message-sender" >To: {{ recipient }}</p>
|
<p class="sms-message-sender" >To: {{ recipient }}</p>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def get_template(
|
|||||||
redact_missing_personalisation=False,
|
redact_missing_personalisation=False,
|
||||||
email_reply_to=None,
|
email_reply_to=None,
|
||||||
sms_sender=None,
|
sms_sender=None,
|
||||||
force_hide_sender=False
|
force_hide_sender=False,
|
||||||
):
|
):
|
||||||
if "email" == template["template_type"]:
|
if "email" == template["template_type"]:
|
||||||
return EmailPreviewTemplate(
|
return EmailPreviewTemplate(
|
||||||
|
|||||||
@@ -22,8 +22,3 @@ def is_less_than_days_ago(date_from_db, number_of_days):
|
|||||||
|
|
||||||
def parse_naive_dt(dt):
|
def parse_naive_dt(dt):
|
||||||
return parser.parse(dt, ignoretz=True)
|
return parser.parse(dt, ignoretz=True)
|
||||||
|
|
||||||
|
|
||||||
def parse_dt(dt):
|
|
||||||
# Parse datetime without ignoring the timezone
|
|
||||||
return parser.parse(dt)
|
|
||||||
|
|||||||
31
poetry.lock
generated
31
poetry.lock
generated
@@ -171,17 +171,17 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3"
|
name = "boto3"
|
||||||
version = "1.34.79"
|
version = "1.34.83"
|
||||||
description = "The AWS SDK for Python"
|
description = "The AWS SDK for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "boto3-1.34.79-py3-none-any.whl", hash = "sha256:265b0b4865e8c07e27abb32a31d2bd9129bb009b1d89ca0783776ec084886123"},
|
{file = "boto3-1.34.83-py3-none-any.whl", hash = "sha256:33cf93f6de5176f1188c923f4de1ae149ed723b89ed12e434f2b2f628491769e"},
|
||||||
{file = "boto3-1.34.79.tar.gz", hash = "sha256:139dd2d94eaa0e3213ff37ba7cf4cb2e3823269178fe8f3e33c965f680a9ddde"},
|
{file = "boto3-1.34.83.tar.gz", hash = "sha256:9733ce811bd82feab506ad9309e375a79cabe8c6149061971c17754ce8997551"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
botocore = ">=1.34.79,<1.35.0"
|
botocore = ">=1.34.83,<1.35.0"
|
||||||
jmespath = ">=0.7.1,<2.0.0"
|
jmespath = ">=0.7.1,<2.0.0"
|
||||||
s3transfer = ">=0.10.0,<0.11.0"
|
s3transfer = ">=0.10.0,<0.11.0"
|
||||||
|
|
||||||
@@ -190,13 +190,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.34.79"
|
version = "1.34.83"
|
||||||
description = "Low-level, data-driven core of boto 3."
|
description = "Low-level, data-driven core of boto 3."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "botocore-1.34.79-py3-none-any.whl", hash = "sha256:a42a014d3dbaa9ef123810592af69f9e55b456c5be3ac9efc037325685519e83"},
|
{file = "botocore-1.34.83-py3-none-any.whl", hash = "sha256:0a3fbbe018416aeefa8978454fb0b8129adbaf556647b72269bf02e4bf1f4161"},
|
||||||
{file = "botocore-1.34.79.tar.gz", hash = "sha256:6b59b0f7de219d383a2a633f6718c2600642ebcb707749dc6c67a6a436474b7a"},
|
{file = "botocore-1.34.83.tar.gz", hash = "sha256:0f302aa76283d4df62b4fbb6d3d20115c1a8957fc02171257fc93904d69d5636"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1080,13 +1080,13 @@ license = ["ukkonen"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.6"
|
version = "3.7"
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
files = [
|
files = [
|
||||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1516,6 +1516,7 @@ files = [
|
|||||||
{file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"},
|
{file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"},
|
||||||
{file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"},
|
{file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"},
|
||||||
{file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"},
|
{file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"},
|
||||||
|
{file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"},
|
||||||
{file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
|
{file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1602,7 +1603,7 @@ requests = ">=2.0.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notifications-utils"
|
name = "notifications-utils"
|
||||||
version = "0.4.5"
|
version = "0.4.6"
|
||||||
description = ""
|
description = ""
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "^3.12.2"
|
python-versions = "^3.12.2"
|
||||||
@@ -1613,8 +1614,8 @@ develop = false
|
|||||||
async-timeout = "^4.0.2"
|
async-timeout = "^4.0.2"
|
||||||
bleach = "^6.1.0"
|
bleach = "^6.1.0"
|
||||||
blinker = "^1.6.2"
|
blinker = "^1.6.2"
|
||||||
boto3 = "^1.34.77"
|
boto3 = "^1.34.83"
|
||||||
botocore = "^1.34.79"
|
botocore = "^1.34.83"
|
||||||
cachetools = "^5.3.0"
|
cachetools = "^5.3.0"
|
||||||
certifi = "^2024.2.2"
|
certifi = "^2024.2.2"
|
||||||
cffi = "^1.16.0"
|
cffi = "^1.16.0"
|
||||||
@@ -1625,7 +1626,7 @@ flask = "^2.3.2"
|
|||||||
flask-redis = "^0.4.0"
|
flask-redis = "^0.4.0"
|
||||||
geojson = "^3.0.1"
|
geojson = "^3.0.1"
|
||||||
govuk-bank-holidays = "^0.14"
|
govuk-bank-holidays = "^0.14"
|
||||||
idna = "^3.6"
|
idna = "^3.7"
|
||||||
itsdangerous = "^2.1.2"
|
itsdangerous = "^2.1.2"
|
||||||
jinja2 = "^3.1.3"
|
jinja2 = "^3.1.3"
|
||||||
jmespath = "^1.0.1"
|
jmespath = "^1.0.1"
|
||||||
@@ -1654,7 +1655,7 @@ werkzeug = "^3.0.1"
|
|||||||
type = "git"
|
type = "git"
|
||||||
url = "https://github.com/GSA/notifications-utils.git"
|
url = "https://github.com/GSA/notifications-utils.git"
|
||||||
reference = "HEAD"
|
reference = "HEAD"
|
||||||
resolved_reference = "7d1d2e9bb3791316231e97433c71da6a70c4d2ab"
|
resolved_reference = "d0db6073406bd160d2007edb9d00e41c9d5d44b7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ def test_form_class_not_mutated(notify_admin):
|
|||||||
(False, "phone number", "sms", "2028675309", None),
|
(False, "phone number", "sms", "2028675309", None),
|
||||||
(False, "phone number", "sms", "+1 (202) 867-5309", None),
|
(False, "phone number", "sms", "+1 (202) 867-5309", None),
|
||||||
(True, "phone number", "sms", "+123", "Not enough digits"),
|
(True, "phone number", "sms", "+123", "Not enough digits"),
|
||||||
(True, "phone number", "sms", "+44(0)7900 900-123", None),
|
|
||||||
(True, "phone number", "sms", "+1-2345-678890", None),
|
(True, "phone number", "sms", "+1-2345-678890", None),
|
||||||
(False, "anything else", "sms", "", "Cannot be empty"),
|
(False, "anything else", "sms", "", "Cannot be empty"),
|
||||||
(False, "anything else", "email", "", "Cannot be empty"),
|
(False, "anything else", "email", "", "Cannot be empty"),
|
||||||
|
|||||||
@@ -354,10 +354,9 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
|
|||||||
"(202) 867-5300 message-2 1 hour ago",
|
"(202) 867-5300 message-2 1 hour ago",
|
||||||
"(202) 867-5300 message-3 1 hour ago",
|
"(202) 867-5300 message-3 1 hour ago",
|
||||||
"(202) 867-5302 message-4 3 hours ago",
|
"(202) 867-5302 message-4 3 hours ago",
|
||||||
"+33 1 12 34 56 78 message-5 5 hours ago",
|
"+33(0)1 12345678 message-5 5 hours ago",
|
||||||
"(202) 555-0104 message-6 7 hours ago",
|
"(202) 555-0104 message-6 7 hours ago",
|
||||||
"(202) 555-0104 message-7 9 hours ago",
|
"(202) 555-0104 message-7 9 hours ago",
|
||||||
"+682 12345 message-8 9 hours ago",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -519,10 +518,10 @@ def test_download_inbox(
|
|||||||
"(202) 867-5300,message-2,07-01-2016 10:59 US/Eastern\r\n"
|
"(202) 867-5300,message-2,07-01-2016 10:59 US/Eastern\r\n"
|
||||||
"(202) 867-5300,message-3,07-01-2016 10:59 US/Eastern\r\n"
|
"(202) 867-5300,message-3,07-01-2016 10:59 US/Eastern\r\n"
|
||||||
"(202) 867-5302,message-4,07-01-2016 08:59 US/Eastern\r\n"
|
"(202) 867-5302,message-4,07-01-2016 08:59 US/Eastern\r\n"
|
||||||
"+33 1 12 34 56 78,message-5,07-01-2016 06:59 US/Eastern\r\n"
|
"+33(0)1 12345678,message-5,07-01-2016 06:59 US/Eastern\r\n"
|
||||||
"(202) 555-0104,message-6,07-01-2016 04:59 US/Eastern\r\n"
|
"(202) 555-0104,message-6,07-01-2016 04:59 US/Eastern\r\n"
|
||||||
"(202) 555-0104,message-7,07-01-2016 02:59 US/Eastern\r\n"
|
"(202) 555-0104,message-7,07-01-2016 02:59 US/Eastern\r\n"
|
||||||
"+682 12345,message-8,07-01-2016 02:59 US/Eastern\r\n"
|
"+68212345,message-8,07-01-2016 02:59 US/Eastern\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -979,7 +979,7 @@ def test_upload_valid_csv_shows_preview_and_table(
|
|||||||
'<td class="table-field-left-aligned"> <div> A </div> </td>',
|
'<td class="table-field-left-aligned"> <div> A </div> </td>',
|
||||||
(
|
(
|
||||||
'<td class="table-field-left-aligned"> '
|
'<td class="table-field-left-aligned"> '
|
||||||
'<div> '
|
"<div> "
|
||||||
"<ul> "
|
"<ul> "
|
||||||
"<li>foo</li> <li>foo</li> <li>foo</li> "
|
"<li>foo</li> <li>foo</li> <li>foo</li> "
|
||||||
"</ul> "
|
"</ul> "
|
||||||
@@ -992,7 +992,7 @@ def test_upload_valid_csv_shows_preview_and_table(
|
|||||||
'<td class="table-field-left-aligned"> <div> B </div> </td>',
|
'<td class="table-field-left-aligned"> <div> B </div> </td>',
|
||||||
(
|
(
|
||||||
'<td class="table-field-left-aligned"> '
|
'<td class="table-field-left-aligned"> '
|
||||||
'<div> '
|
"<div> "
|
||||||
"<ul> "
|
"<ul> "
|
||||||
"<li>foo</li> <li>foo</li> <li>foo</li> "
|
"<li>foo</li> <li>foo</li> <li>foo</li> "
|
||||||
"</ul> "
|
"</ul> "
|
||||||
@@ -1005,7 +1005,7 @@ def test_upload_valid_csv_shows_preview_and_table(
|
|||||||
'<td class="table-field-left-aligned"> <div> C </div> </td>',
|
'<td class="table-field-left-aligned"> <div> C </div> </td>',
|
||||||
(
|
(
|
||||||
'<td class="table-field-left-aligned"> '
|
'<td class="table-field-left-aligned"> '
|
||||||
'<div> '
|
"<div> "
|
||||||
"<ul> "
|
"<ul> "
|
||||||
"<li>foo</li> <li>foo</li> "
|
"<li>foo</li> <li>foo</li> "
|
||||||
"</ul> "
|
"</ul> "
|
||||||
|
|||||||
188
tests/end_to_end/test_create_new_template.py
Normal file
188
tests/end_to_end/test_create_new_template.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
E2E_TEST_URI = os.getenv("NOTIFY_E2E_TEST_URI")
|
||||||
|
|
||||||
|
|
||||||
|
def _setup(page):
|
||||||
|
# Prepare for adding a new service later in the test.
|
||||||
|
current_date_time = datetime.datetime.now()
|
||||||
|
new_service_name = "E2E Federal Test Service {now} - {browser_type}".format(
|
||||||
|
now=current_date_time.strftime("%m/%d/%Y %H:%M:%S"),
|
||||||
|
browser_type=page.context.browser.browser_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
page.goto(f"{E2E_TEST_URI}/accounts")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
# Check the page title exists and matches what we expect.
|
||||||
|
expect(page).to_have_title(re.compile("Choose service"))
|
||||||
|
|
||||||
|
# Check for the sign in heading.
|
||||||
|
sign_in_heading = page.get_by_role("heading", name="Choose service")
|
||||||
|
expect(sign_in_heading).to_be_visible()
|
||||||
|
|
||||||
|
# Retrieve some prominent elements on the page for testing.
|
||||||
|
add_service_button = page.get_by_role(
|
||||||
|
"button", name=re.compile("Add a new service")
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(add_service_button).to_be_visible()
|
||||||
|
|
||||||
|
existing_service_link = page.get_by_role("link", name=new_service_name)
|
||||||
|
|
||||||
|
# Check to see if the service was already created - if so, we should fail.
|
||||||
|
# TODO: Figure out how to make this truly isolated, and/or work in a
|
||||||
|
# delete service workflow.
|
||||||
|
expect(existing_service_link).to_have_count(0)
|
||||||
|
|
||||||
|
# Click on add a new service.
|
||||||
|
add_service_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check for the sign in heading.
|
||||||
|
about_heading = page.get_by_role("heading", name="About your service")
|
||||||
|
expect(about_heading).to_be_visible()
|
||||||
|
|
||||||
|
# Retrieve some prominent elements on the page for testing.
|
||||||
|
service_name_input = page.locator('xpath=//input[@name="name"]')
|
||||||
|
add_service_button = page.get_by_role("button", name=re.compile("Add service"))
|
||||||
|
|
||||||
|
expect(service_name_input).to_be_visible()
|
||||||
|
expect(add_service_button).to_be_visible()
|
||||||
|
|
||||||
|
# Fill in the form.
|
||||||
|
service_name_input.fill(new_service_name)
|
||||||
|
|
||||||
|
# Click on add service.
|
||||||
|
add_service_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check for the service name title and heading.
|
||||||
|
service_heading = page.get_by_text(new_service_name, exact=True)
|
||||||
|
|
||||||
|
expect(service_heading).to_be_visible()
|
||||||
|
expect(page).to_have_title(re.compile(new_service_name))
|
||||||
|
|
||||||
|
return new_service_name
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_template(page):
|
||||||
|
|
||||||
|
current_service_link = page.get_by_text("Current service")
|
||||||
|
expect(current_service_link).to_be_visible()
|
||||||
|
current_service_link.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
send_messages_button = page.get_by_role("link", name="Send messages")
|
||||||
|
expect(send_messages_button).to_be_visible()
|
||||||
|
send_messages_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
create_template_button = page.get_by_role("button", name="New template")
|
||||||
|
expect(create_template_button).to_be_visible()
|
||||||
|
create_template_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
start_with_a_blank_template_radio = page.get_by_text("Start with a blank template")
|
||||||
|
expect(start_with_a_blank_template_radio).to_be_visible()
|
||||||
|
start_with_a_blank_template_radio.click()
|
||||||
|
|
||||||
|
continue_button = page.get_by_role("button", name="Continue")
|
||||||
|
|
||||||
|
# continue_button = page.get_by_text("Continue")
|
||||||
|
expect(continue_button).to_be_visible()
|
||||||
|
continue_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
template_name_input = page.get_by_text("Template name")
|
||||||
|
expect(template_name_input).to_be_visible()
|
||||||
|
template_name = str(uuid.uuid4())
|
||||||
|
template_name_input.fill(template_name)
|
||||||
|
message_input = page.get_by_role("textbox", name="Message")
|
||||||
|
expect(message_input).to_be_visible()
|
||||||
|
message = "Test message for e2e test"
|
||||||
|
message_input.fill(message)
|
||||||
|
|
||||||
|
save_button = page.get_by_text("Save")
|
||||||
|
expect(save_button).to_be_visible()
|
||||||
|
save_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
use_this_template_button = page.get_by_text("Use this template")
|
||||||
|
expect(use_this_template_button).to_be_visible()
|
||||||
|
use_this_template_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
use_my_phone_number_link = page.get_by_text("Use my phone number")
|
||||||
|
expect(use_my_phone_number_link).to_be_visible()
|
||||||
|
use_my_phone_number_link.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
preview_button = page.get_by_text("Preview")
|
||||||
|
expect(preview_button).to_be_visible()
|
||||||
|
preview_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# We are not going to send the message for this test, we just want to confirm
|
||||||
|
# that the template has been created and we are now seeing the message from the
|
||||||
|
# template in the preview.
|
||||||
|
assert "Test message for e2e test" in page.content()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_new_template(authenticated_page):
|
||||||
|
page = authenticated_page
|
||||||
|
|
||||||
|
_setup(page)
|
||||||
|
|
||||||
|
create_new_template(page)
|
||||||
|
|
||||||
|
_teardown(page)
|
||||||
|
|
||||||
|
|
||||||
|
def _teardown(page):
|
||||||
|
page.click("text='Settings'")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
page.click("text='Delete this service'")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
page.click("text='Yes, delete'")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
# Check the page title exists and matches what we expect.
|
||||||
|
expect(page).to_have_title(re.compile("Choose service"))
|
||||||
339
tests/end_to_end/test_send_message_from_existing_template.py
Normal file
339
tests/end_to_end/test_send_message_from_existing_template.py
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
E2E_TEST_URI = os.getenv("NOTIFY_E2E_TEST_URI")
|
||||||
|
|
||||||
|
|
||||||
|
def _setup(page):
|
||||||
|
# Prepare for adding a new service later in the test.
|
||||||
|
current_date_time = datetime.datetime.now()
|
||||||
|
new_service_name = "E2E Federal Test Service {now} - {browser_type}".format(
|
||||||
|
now=current_date_time.strftime("%m/%d/%Y %H:%M:%S"),
|
||||||
|
browser_type=page.context.browser.browser_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
page.goto(f"{E2E_TEST_URI}/accounts")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
# Check the page title exists and matches what we expect.
|
||||||
|
expect(page).to_have_title(re.compile("Choose service"))
|
||||||
|
|
||||||
|
# Check for the sign in heading.
|
||||||
|
sign_in_heading = page.get_by_role("heading", name="Choose service")
|
||||||
|
expect(sign_in_heading).to_be_visible()
|
||||||
|
|
||||||
|
# Retrieve some prominent elements on the page for testing.
|
||||||
|
add_service_button = page.get_by_role(
|
||||||
|
"button", name=re.compile("Add a new service")
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(add_service_button).to_be_visible()
|
||||||
|
|
||||||
|
existing_service_link = page.get_by_role("link", name=new_service_name)
|
||||||
|
|
||||||
|
# Check to see if the service was already created - if so, we should fail.
|
||||||
|
# TODO: Figure out how to make this truly isolated, and/or work in a
|
||||||
|
# delete service workflow.
|
||||||
|
expect(existing_service_link).to_have_count(0)
|
||||||
|
|
||||||
|
# Click on add a new service.
|
||||||
|
add_service_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check for the sign in heading.
|
||||||
|
about_heading = page.get_by_role("heading", name="About your service")
|
||||||
|
expect(about_heading).to_be_visible()
|
||||||
|
|
||||||
|
# Retrieve some prominent elements on the page for testing.
|
||||||
|
service_name_input = page.locator('xpath=//input[@name="name"]')
|
||||||
|
add_service_button = page.get_by_role("button", name=re.compile("Add service"))
|
||||||
|
|
||||||
|
expect(service_name_input).to_be_visible()
|
||||||
|
expect(add_service_button).to_be_visible()
|
||||||
|
|
||||||
|
# Fill in the form.
|
||||||
|
service_name_input.fill(new_service_name)
|
||||||
|
|
||||||
|
# Click on add service.
|
||||||
|
add_service_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check for the service name title and heading.
|
||||||
|
service_heading = page.get_by_text(new_service_name, exact=True)
|
||||||
|
|
||||||
|
expect(service_heading).to_be_visible()
|
||||||
|
expect(page).to_have_title(re.compile(new_service_name))
|
||||||
|
|
||||||
|
return new_service_name
|
||||||
|
|
||||||
|
|
||||||
|
def handle_no_existing_template_case(page):
|
||||||
|
"""
|
||||||
|
This is the test path for local development. Developers have their own
|
||||||
|
databases and typically they have service or two already created, which
|
||||||
|
means they won't see the "Example text message template" which is done
|
||||||
|
as an introduction. Instead they will see "Create your first template".
|
||||||
|
Note that this test goes all the way through sending and downloading the
|
||||||
|
report and verifying the phone number, etc. in the report. This is because
|
||||||
|
developer systems are fully connected to AWS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
create_template_button = page.get_by_text("Create your first template")
|
||||||
|
expect(create_template_button).to_be_visible()
|
||||||
|
create_template_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
new_template_button = page.get_by_text("New template")
|
||||||
|
expect(new_template_button).to_be_visible()
|
||||||
|
new_template_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
start_with_a_blank_template_radio = page.get_by_text("Start with a blank template")
|
||||||
|
expect(start_with_a_blank_template_radio).to_be_visible()
|
||||||
|
start_with_a_blank_template_radio.click()
|
||||||
|
|
||||||
|
continue_button = page.get_by_role("button", name="Continue")
|
||||||
|
|
||||||
|
# continue_button = page.get_by_text("Continue")
|
||||||
|
expect(continue_button).to_be_visible()
|
||||||
|
continue_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
template_name_input = page.get_by_text("Template name")
|
||||||
|
expect(template_name_input).to_be_visible()
|
||||||
|
template_name = str(uuid.uuid4())
|
||||||
|
template_name_input.fill(template_name)
|
||||||
|
message_input = page.get_by_role("textbox", name="Message")
|
||||||
|
expect(message_input).to_be_visible()
|
||||||
|
message = "Test message"
|
||||||
|
message_input.fill(message)
|
||||||
|
|
||||||
|
save_button = page.get_by_text("Save")
|
||||||
|
expect(save_button).to_be_visible()
|
||||||
|
save_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
use_this_template_button = page.get_by_text("Use this template")
|
||||||
|
expect(use_this_template_button).to_be_visible()
|
||||||
|
use_this_template_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
use_my_phone_number_link = page.get_by_text("Use my phone number")
|
||||||
|
expect(use_my_phone_number_link).to_be_visible()
|
||||||
|
use_my_phone_number_link.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
preview_button = page.get_by_text("Preview")
|
||||||
|
expect(preview_button).to_be_visible()
|
||||||
|
preview_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
send_button = page.get_by_role("button", name="Send")
|
||||||
|
expect(send_button).to_be_visible()
|
||||||
|
send_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
dashboard_button = page.get_by_text("Dashboard")
|
||||||
|
expect(dashboard_button).to_be_visible()
|
||||||
|
dashboard_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
download_link = page.get_by_text("Download")
|
||||||
|
expect(download_link).to_be_visible()
|
||||||
|
|
||||||
|
# Start waiting for the download
|
||||||
|
with page.expect_download() as download_info:
|
||||||
|
# Perform the action that initiates download
|
||||||
|
download_link.click()
|
||||||
|
download = download_info.value
|
||||||
|
# Wait for the download process to complete and save the downloaded file somewhere
|
||||||
|
download.save_as("download_test_file")
|
||||||
|
f = open("download_test_file", "r")
|
||||||
|
|
||||||
|
content = f.read()
|
||||||
|
f.close()
|
||||||
|
# We don't want to wait 5 minutes to get a response from AWS about the message we sent
|
||||||
|
# So we are using this invalid phone number the e2e_test_user signed up with (12025555555)
|
||||||
|
# to shortcircuit the sending process. Our phone number validator will insta-fail the
|
||||||
|
# message and it won't be sent, but the report will still be generated, which is all
|
||||||
|
# we care about here.
|
||||||
|
assert (
|
||||||
|
"Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time"
|
||||||
|
in content
|
||||||
|
)
|
||||||
|
assert "12025555555" in content
|
||||||
|
assert "one-off-e2e_test_user" in content
|
||||||
|
os.remove("download_test_file")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_existing_template_case(page):
|
||||||
|
"""
|
||||||
|
This is the test path used in the Github action. Right now the e2e tests
|
||||||
|
are not connected to AWS, so we can't actually send messages. Hence, the
|
||||||
|
test stops when it verifies that the 'Send' button exists on the page. In
|
||||||
|
the future we would like to change the way e2e tests run so they run against
|
||||||
|
staging, in which case either the code in this method can be uncommented,
|
||||||
|
or perhaps this path will be unnecessary (because staging is not completely clean)
|
||||||
|
and we will just use the same path as for local development.
|
||||||
|
"""
|
||||||
|
existing_template_link = page.get_by_text("Example text message template")
|
||||||
|
expect(existing_template_link).to_be_visible()
|
||||||
|
existing_template_link.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
continue_button = page.get_by_role("button", name="Continue")
|
||||||
|
|
||||||
|
# continue_button = page.get_by_text("Continue")
|
||||||
|
expect(continue_button).to_be_visible()
|
||||||
|
continue_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
continue_button = page.get_by_role("button", name="Continue")
|
||||||
|
|
||||||
|
# continue_button = page.get_by_text("Continue")
|
||||||
|
expect(continue_button).to_be_visible()
|
||||||
|
continue_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# day_of_week_input = page.locator('xpath=//input[@name="day of week"]')
|
||||||
|
# day_of_week_input = page.get_by_text("day of week")
|
||||||
|
day_of_week_input = page.get_by_role("textbox", name="day of week")
|
||||||
|
expect(day_of_week_input).to_be_visible()
|
||||||
|
day_of_week_input.fill("Monday")
|
||||||
|
|
||||||
|
continue_button = page.get_by_role("button", name="Continue")
|
||||||
|
|
||||||
|
# continue_button = page.get_by_text("Continue")
|
||||||
|
expect(continue_button).to_be_visible()
|
||||||
|
continue_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
color_input = page.get_by_role("textbox", name="color")
|
||||||
|
expect(color_input).to_be_visible()
|
||||||
|
color_input.fill("Green")
|
||||||
|
|
||||||
|
# continue_button = page.get_by_text("Continue")
|
||||||
|
expect(continue_button).to_be_visible()
|
||||||
|
continue_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
preview_button = page.get_by_text("Preview")
|
||||||
|
expect(preview_button).to_be_visible()
|
||||||
|
preview_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
send_button = page.get_by_role("button", name="Send")
|
||||||
|
expect(send_button).to_be_visible()
|
||||||
|
# send_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
# page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# dashboard_button = page.get_by_text("Dashboard")
|
||||||
|
# expect(dashboard_button).to_be_visible()
|
||||||
|
# dashboard_button.click()
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
# page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# download_link = page.get_by_text("Download")
|
||||||
|
# expect(download_link).to_be_visible()
|
||||||
|
|
||||||
|
# Start waiting for the download
|
||||||
|
# with page.expect_download() as download_info:
|
||||||
|
# Perform the action that initiates download
|
||||||
|
# download_link.click()
|
||||||
|
# download = download_info.value
|
||||||
|
# Wait for the download process to complete and save the downloaded file somewhere
|
||||||
|
# download.save_as("download_test_file")
|
||||||
|
# f = open("download_test_file", "r")
|
||||||
|
|
||||||
|
# content = f.read()
|
||||||
|
# f.close()
|
||||||
|
# We don't want to wait 5 minutes to get a response from AWS about the message we sent
|
||||||
|
# So we are using this invalid phone number the e2e_test_user signed up with (12025555555)
|
||||||
|
# to shortcircuit the sending process. Our phone number validator will insta-fail the
|
||||||
|
# message and it won't be sent, but the report will still be generated, which is all
|
||||||
|
# we care about here.
|
||||||
|
# assert (
|
||||||
|
# "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time"
|
||||||
|
# in content
|
||||||
|
# )
|
||||||
|
# assert "12025555555" in content
|
||||||
|
# assert "one-off-e2e_test_user" in content
|
||||||
|
# os.remove("download_test_file")
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_message_from_existing_template(authenticated_page):
|
||||||
|
page = authenticated_page
|
||||||
|
|
||||||
|
_setup(page)
|
||||||
|
|
||||||
|
if page.get_by_text("Create your first template").count() > 0:
|
||||||
|
handle_no_existing_template_case(page)
|
||||||
|
else:
|
||||||
|
handle_existing_template_case(page)
|
||||||
|
|
||||||
|
_teardown(page)
|
||||||
|
|
||||||
|
|
||||||
|
def _teardown(page):
|
||||||
|
page.click("text='Settings'")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
page.click("text='Delete this service'")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
page.click("text='Yes, delete'")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
# Check to make sure that we've arrived at the next page.
|
||||||
|
# Check the page title exists and matches what we expect.
|
||||||
|
expect(page).to_have_title(re.compile("Choose service"))
|
||||||
Reference in New Issue
Block a user