diff --git a/app/assets/sass/uswds/_legacy-styles.scss b/app/assets/sass/uswds/_legacy-styles.scss index 3b09e2218..b96a24844 100644 --- a/app/assets/sass/uswds/_legacy-styles.scss +++ b/app/assets/sass/uswds/_legacy-styles.scss @@ -48,13 +48,13 @@ } } -.sms-message-sender { - margin: units(1) 0 0; +.sms-message-sender, .sms-message-file-name, .sms-message-scheduler, .sms-message-template, .sms-message-sender { + margin:0.25rem 0 0; } .sms-message-recipient { color: color('gray-cool-90'); - margin: 0 0 units(1); + margin: units(1) 0 units(1); } .sms-message-status { @@ -131,7 +131,7 @@ &-label, &-button-label { font-weight: bold; - font-size: 19px; + font-size: 19px; display: block; margin: 0 0 10px 0; } diff --git a/app/main/forms.py b/app/main/forms.py index 2e6655cc5..70dbfa37f 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1226,7 +1226,11 @@ class CsvUploadForm(StripWhitespaceForm): validators=[ DataRequired(message="Please pick a file"), CsvFileValidator(), - FileSize(max_size=10e6, message="File must be smaller than 10Mb"), # 10Mb + FileSize( + max_size=10e6, + message="File must be smaller than 10Mb. If you are trying to upload an Excel file, \ + please export the contents in the CSV format and then try again.", + ), # 10Mb ], ) diff --git a/app/main/views/send.py b/app/main/views/send.py index 6c786e3ac..b1cea53a8 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -6,6 +6,7 @@ from zipfile import BadZipFile from flask import abort, flash, redirect, render_template, request, session, url_for from flask_login import current_user +from markupsafe import Markup from notifications_python_client.errors import HTTPError from notifications_utils import SMS_CHAR_COUNT_LIMIT from notifications_utils.insensitive_dict import InsensitiveDict @@ -151,8 +152,11 @@ def send_messages(service_id, template_id): # just show the first error, as we don't expect the form to have more # than one, since it only has one field first_field_errors = list(form.errors.values())[0] - flash(first_field_errors[0]) - + error_message = '' + error_message = f"{error_message}{first_field_errors[0]}" + error_message = f"{error_message}" + error_message = Markup(error_message) + flash(error_message) column_headings = get_spreadsheet_column_headings_from_template(template) return render_template( @@ -504,13 +508,18 @@ def _check_messages(service_id, template_id, upload_id, preview_row): template = get_template( db_template, current_service, - show_recipient=True, + show_recipient=False, email_reply_to=email_reply_to, sms_sender=sms_sender, ) + simplifed_template = get_template( + db_template, + current_service, + show_recipient=False, + ) recipients = RecipientCSV( contents, - template=template, + template=template or simplifed_template, max_initial_rows_shown=50, max_errors_shown=50, guestlist=( @@ -530,11 +539,20 @@ def _check_messages(service_id, template_id, upload_id, preview_row): back_link = url_for( "main.send_one_off", service_id=service_id, template_id=template.id ) + back_link_from_preview = url_for( + "main.send_one_off", service_id=service_id, template_id=template.id + ) choose_time_form = None else: back_link = url_for( "main.send_messages", service_id=service_id, template_id=template.id ) + back_link_from_preview = url_for( + "main.check_messages", + service_id=service_id, + template_id=template.id, + upload_id=upload_id, + ) choose_time_form = ChooseTimeForm() if preview_row < 2: @@ -542,6 +560,7 @@ def _check_messages(service_id, template_id, upload_id, preview_row): if preview_row < len(recipients) + 2: template.values = recipients[preview_row - 2].recipient_and_personalisation + simplifed_template.values = recipients[preview_row - 2].recipient_and_personalisation elif preview_row > 2: abort(404) @@ -562,11 +581,14 @@ def _check_messages(service_id, template_id, upload_id, preview_row): remaining_messages=remaining_messages, choose_time_form=choose_time_form, back_link=back_link, + back_link_from_preview=back_link_from_preview, first_recipient_column=recipients.recipient_column_headers[0], preview_row=preview_row, sent_previously=job_api_client.has_sent_previously( service_id, template.id, db_template["version"], original_file_name ), + template_id=template_id, + simplifed_template=simplifed_template, ) @@ -614,13 +636,34 @@ def check_messages(service_id, template_id, upload_id, row_index=2): return render_template("views/check/ok.html", **data) +@main.route( + "/services///check//preview", + methods=["POST"], +) +@main.route( + "/services///check//preview/row-", + methods=["POST"], +) +@user_has_permissions("send_messages", restrict_admin_usage=True) +def preview_job(service_id, template_id, upload_id, row_index=2): + session["scheduled_for"] = request.form.get("scheduled_for", "") + data = _check_messages(service_id, template_id, upload_id, row_index) + + return render_template( + "views/check/preview.html", + scheduled_for=session["scheduled_for"], + **data, + ) + + @main.route("/services//start-job/", methods=["POST"]) @user_has_permissions("send_messages", restrict_admin_usage=True) def start_job(service_id, upload_id): + scheduled_for = session.pop("scheduled_for", None) job_api_client.create_job( upload_id, service_id, - scheduled_for=request.form.get("scheduled_for", ""), + scheduled_for=scheduled_for, ) session.pop("sender_id", None) @@ -679,7 +722,20 @@ def get_send_test_page_title(template_type, entering_recipient, name=None): return "Personalize this message" -def get_back_link(service_id, template, step_index, placeholders=None): +def get_back_link( + service_id, + template, + step_index, + placeholders=None, + preview=False, +): + if preview: + return url_for( + "main.check_notification", + service_id=service_id, + template_id=template.id, + ) + if step_index == 0: if should_skip_template_page(template._template): return url_for( @@ -779,11 +835,18 @@ def _check_notification(service_id, template_id, exception=None): email_reply_to=email_reply_to, sms_sender=sms_sender, ) - + simplifed_template = get_template( + db_template, + current_service, + ) placeholders = fields_to_fill_in(template) back_link = get_back_link(service_id, template, len(placeholders), placeholders) + back_link_from_preview = get_back_link( + service_id, template, len(placeholders), placeholders, preview=True + ) + choose_time_form = ChooseTimeForm() if (not session.get("recipient")) or not all_placeholders_in_session( @@ -797,8 +860,10 @@ def _check_notification(service_id, template_id, exception=None): return dict( template=template, back_link=back_link, + back_link_from_preview=back_link_from_preview, choose_time_form=choose_time_form, **(get_template_error_dict(exception) if exception else {}), + simplifed_template=simplifed_template ) @@ -828,12 +893,39 @@ def get_template_error_dict(exception): } +@main.route( + "/services//template//notification/check/preview", + methods=["POST"], +) +@user_has_permissions("send_messages", restrict_admin_usage=True) +def preview_notification(service_id, template_id): + recipient = get_recipient() + if not recipient: + return redirect( + url_for( + ".send_one_off", + service_id=service_id, + template_id=template_id, + ) + ) + + session["scheduled_for"] = request.form.get("scheduled_for", "") + + return render_template( + "views/notifications/preview.html", + **_check_notification(service_id, template_id), + scheduled_for=session["scheduled_for"], + recipient=recipient, + ) + + @main.route( "/services//template//notification/check", methods=["POST"], ) @user_has_permissions("send_messages", restrict_admin_usage=True) def send_notification(service_id, template_id): + scheduled_for = session.pop("scheduled_for", "") recipient = get_recipient() if not recipient: return redirect( @@ -868,7 +960,7 @@ def send_notification(service_id, template_id): job_api_client.create_job( upload_id, service_id, - scheduled_for=request.form.get("scheduled_for", ""), + scheduled_for=scheduled_for, template_id=template_id, original_file_name=filename, notification_count=1, diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index c6eaee87e..d39cb89af 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -53,22 +53,19 @@ def _get_access_token(code, state): # JWT expiration time (10 minute maximum) "exp": int(time.time()) + (10 * 60), } - current_app.logger.warning(f"Here is the raw payload {payload}") token = jwt.encode(payload, keystring, algorithm="RS256") base_url = f"{access_token_url}?" cli_assert = f"client_assertion={token}" cli_assert_type = "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" code_param = f"code={code}" url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code" - current_app.logger.info(f"This is the url we use to get the access token: {url}") headers = {"Authorization": "Bearer %s" % token} response = requests.post(url, headers=headers) - current_app.logger.info(f"GOT A RESPONSE {response.json()}") access_token = response.json()["access_token"] return access_token -def _get_user_email(access_token): +def _get_user_email_and_uuid(access_token): headers = {"Authorization": "Bearer %s" % access_token} user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL") user_attributes = requests.get( @@ -76,7 +73,8 @@ def _get_user_email(access_token): headers=headers, ) user_email = user_attributes.json()["email"] - return user_email + user_uuid = user_attributes.json()["sub"] + return user_email, user_uuid @main.route("/sign-in", methods=(["GET", "POST"])) @@ -88,11 +86,11 @@ def sign_in(): login_gov_error = request.args.get("error") if code and state: access_token = _get_access_token(code, state) - user_email = _get_user_email(access_token) + user_email, user_uuid = _get_user_email_and_uuid(access_token) redirect_url = request.args.get("next") # activate the user - user = user_api_client.get_user_by_email(user_email) + user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email) activate_user(user["id"]) return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url)) diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index bfe2f7182..a17059e52 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -44,6 +44,16 @@ class UserApiClient(NotifyAdminAPIClient): user_data = self.post("/user/email", data={"email": email_address}) return user_data["data"] + def get_user_by_uuid_or_email(self, user_uuid, email_address): + + user_data = self.post( + "/user/get-login-gov-user", + data={"login_uuid": user_uuid, "email": email_address}, + ) + if user_data is None: + raise Exception("User not found") + return user_data["data"] + def get_user_by_email_or_none(self, email_address): try: return self.get_user_by_email(email_address) diff --git a/app/templates/new/base.html b/app/templates/new/base.html new file mode 100644 index 000000000..b175cff51 --- /dev/null +++ b/app/templates/new/base.html @@ -0,0 +1,372 @@ +{% from "../components/banner.html" import banner %} +{% from "../components/components/skip-link/macro.njk" import usaSkipLink -%} +{% from "../components/components/header/macro.njk" import usaHeader -%} +{% from "../components/components/footer/macro.njk" import usaFooter -%} + + + + + + + + {% block pageTitle %} + {% block per_page_title %} {% endblock %}Notify.gov + <!-- on templates that were using content_template.html, we might need to use the {{ content_page_title }} variable for the per_page_title --> + {% endblock %} + + + + + {% if config['NR_MONITOR_ON'] %} + {% include "partials/newrelic.html" -%} + {% endif %} + {# Ensure that older IE versions always render with the correct rendering engine #} + + {% block headIcons %} + + + + + {% endblock %} + + {% block head %} + + {% block extra_stylesheets %} + {% endblock %} + {% if g.hide_from_search_engines %} + + {% endif %} + + {# The default og:image is added below head so that scrapers see any custom metatags first, and this is just a fallback #} + {% block meta_format_detection %} + + {% endblock %} + {% block meta %} + + + {% endblock %} + + {% endblock %} + + + + + {% block bodyStart %} + {% block extra_javascripts_before_body %} + + + + {% endblock %} + {% endblock %} + + {% block skipLink %} + {{ usaSkipLink({ + "href": '#main-content', + "text": 'Skip to main content' + }) }} + {% endblock %} + + + {% block header %} + {% if current_user.is_authenticated %} + {% if current_user.platform_admin %} + {% set navigation = [ + { + "href": url_for("main.show_accounts_or_dashboard"), + "text": "Current service", + "active": header_navigation.is_selected('accounts-or-dashboard') + }, + { + "href": url_for('main.get_started'), + "text": "Using Notify", + "active": header_navigation.is_selected('using_notify') + }, + { + "href": url_for('main.features'), + "text": "Features", + "active": header_navigation.is_selected('features') + }, + { + "href": url_for('main.platform_admin_splash_page'), + "text": "Platform admin", + "active": header_navigation.is_selected('platform-admin') + }, + { + "href": url_for('main.support'), + "text": "Contact us", + "active": header_navigation.is_selected('support') + } + ] %} + {% if current_service %} + {% set secondaryNavigation = [ + { + "href": url_for('main.service_settings', service_id=current_service.id), + "text": "Settings", + "active": secondary_navigation.is_selected('settings') + }, + { + "href": url_for('main.sign_out'), + "text": "Sign out" + } + ] %} + {% else %} + {% set secondaryNavigation = [ + { + "href": url_for('main.sign_out'), + "text": "Sign out" + } + ] %} + {% endif %} + {% else %} + {% set navigation = [ + { + "href": url_for("main.show_accounts_or_dashboard"), + "text": "Current service", + "active": header_navigation.is_selected('accounts-or-dashboard') + }, + { + "href": url_for('main.get_started'), + "text": "Using Notify", + "active": header_navigation.is_selected('using_notify') + }, + { + "href": url_for('main.features'), + "text": "Features", + "active": header_navigation.is_selected('features') + }, + { + "href": url_for('main.support'), + "text": "Contact us", + "active": header_navigation.is_selected('support') + }, + { + "href": url_for('main.user_profile'), + "text": "User profile", + "active": header_navigation.is_selected('user-profile') + } + ] %} + {% if current_service %} + {% set secondaryNavigation = [ + { + "href": url_for('main.service_settings', service_id=current_service.id), + "text": "Settings", + "active": secondary_navigation.is_selected('settings') + }, + { + "href": url_for('main.sign_out'), + "text": "Sign out" + } + ] %} + {% else %} + {% set secondaryNavigation = [ + { + "href": url_for('main.sign_out'), + "text": "Sign out" + } + ] %} + {% endif %} + {% endif %} + {% else %} + + {# {% set navigation = [ + { + "href": url_for('main.get_started'), + "text": "Using Notify", + "active": header_navigation.is_selected('using_notify') + }, + { + "href": url_for('main.features'), + "text": "Features", + "active": header_navigation.is_selected('features') + }, + { + "href": url_for('main.support'), + "text": "Contact us", + "active": header_navigation.is_selected('support') + }, + { + "href": url_for('main.sign_in'), + "text": "Sign in", + "active": header_navigation.is_selected('sign-in') + } + ] %} #} + {% endif %} + + {{ usaHeader({ + "homepageUrl": url_for('main.show_accounts_or_dashboard'), + "productName": "Notify", + "navigation": navigation, + "navigationClasses": "govuk-header__navigation--end", + "secondaryNavigation": secondaryNavigation, + "assetsPath": asset_path + "images" + }) }} + {% endblock %} + + + + {% block main %} +
+ {% block beforeContent %} + {% block backLink %}{% endblock %} + {% endblock %} + {% block mainClasses %} + +
+ {% endblock %} + {% block content %} + {% block flash_messages %} + + {% endblock %} + {% block maincolumn_content %} + {% block fromContentTemplatetwoColumnGrid %} +
+ {% if navigation_links %} +
+ {{ sub_navigation(navigation_links) }} +
+
+ {% else %} +
+ {% endif %} + {% block content_column_content %}{% endblock %} +
+
+ + {% endblock %} + {% endblock %} + {% endblock %} +
+
+ {% endblock %} + + + + {% block footer %} + + {% if current_service and current_service.research_mode %} + {% set meta_suffix = 'Built by the Technology Transformation Servicesresearch mode' %} + {% else %} + {% set meta_suffix = 'Built by the Technology Transformation Services' %} + {% endif %} + + {{ usaFooter({ + "classes": "js-footer", + "navigation": [ + { + "title": "About Notify", + "columns": 1, + "items": [ + { + "href": url_for("main.features"), + "text": "Features" + }, + { + "href": url_for("main.roadmap"), + "text": "Roadmap" + }, + { + "href": url_for("main.security"), + "text": "Security" + }, + { + "href": url_for("main.terms"), + "text": "Terms of use" + }, + ] + }, + { + "title": "Using Notify", + "columns": 1, + "items": [ + { + "href": url_for("main.get_started"), + "text": "Get started" + }, + { + "href": url_for("main.pricing"), + "text": "Pricing" + }, + { + "href": url_for("main.trial_mode_new"), + "text": "Trial mode" + }, + { + "href": url_for("main.message_status"), + "text": "Delivery status" + }, + { + "href": url_for("main.guidance_index"), + "text": "Guidance" + }, + { + "href": url_for("main.documentation"), + "text": "API documentation" + } + ] + }, + { + "title": "Support", + "columns": 1, + "items": [ + { + "href": url_for('main.support'), + "text": "Contact us" + }, + ] + }, + ], + "meta": { + "items": meta_items, + "html": meta_suffix + } + }) }} + + {% if current_user.is_authenticated %} + {% block sessionUserWarning %} + +
+
+

+ Your session will end soon. + Please choose to extend your session or sign out. Your session will expire in 5 minutes or less. +

+
+

You have been inactive for too long. + Your session will expire in . +

+
+ +
+
+
+ {% endblock %} + {% endif %} + + {% endblock %} + + + {% block bodyEnd %} + {% block extra_javascripts %} + {% endblock %} + + + + + {% endblock %} + + diff --git a/app/templates/new/layouts/org_template.html b/app/templates/new/layouts/org_template.html new file mode 100644 index 000000000..5e04003fd --- /dev/null +++ b/app/templates/new/layouts/org_template.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block per_page_title %} + {% block org_page_title %}{% endblock %} – {{ current_org.name }} +{% endblock %} + +{% block main %} +
+ +
+
+ {% include "org_nav.html" %} +
+
+ {% block beforeContent %} + {% block backLink %}{% endblock %} + {% endblock %} +
+ {% block content %} + {% include 'flash_messages.html' %} + {% block maincolumn_content %}{% endblock %} + {% endblock %} +
+
+
+
+{% endblock %} diff --git a/app/templates/new/layouts/withnav_template.html b/app/templates/new/layouts/withnav_template.html new file mode 100644 index 000000000..412a004d0 --- /dev/null +++ b/app/templates/new/layouts/withnav_template.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block per_page_title %} + {% block service_page_title %}{% endblock %} – {{ current_service.name }} +{% endblock %} + +{% block main %} +
+ {% block serviceNavigation %} + {% include "service_navigation.html" %} + {% endblock %} + +
+ {% if help %} +
+ {% else %} +
+ {% endif %} + {% block sideNavigation %} + {% include "main_nav.html" %} + + {% endblock %} +
+ {% if help %} +
+ {% else %} +
+ {% endif %} + {% block beforeContent %} + {% block backLink %}{% endblock %} + {% endblock %} +
+ {% block content %} + {% include 'flash_messages.html' %} + {% block maincolumn_content %}{% endblock %} + {% endblock %} +
+
+
+
+{% endblock %} diff --git a/app/templates/new/templates_glossary.md b/app/templates/new/templates_glossary.md new file mode 100644 index 000000000..81325e26c --- /dev/null +++ b/app/templates/new/templates_glossary.md @@ -0,0 +1,37 @@ + +# New Templates Glossary + +This document serves as a glossary for the templates directory structure of the project. + +## Directory Structure + +- `/templates` + - `base.html`: The main base template from which all other templates inherit. This template is a combination of `main_template`, `admin_template`, `withoutnav_template` and `content_template`. + - **/layouts**: Contains base templates and shared layouts used across the site. Simply put, it defines the overall structure or skeleton of the application (less frequently revised). + - `withnav_template.html`: A variation of the base layout that includes a sidebar. + - `org_template.html`: A variaton of the withnav_template + - **/components**: Houses reusable UI components that can be included in multiple templates and can be tailored with different content or links depending on the context.(more frequently revised or customized) + - `header.html`: Template for the site's header, included in `base.html`. + - `footer.html`: Template for the site's footer, included in `base.html`. + - **/views** (or **/pages**): Individual page templates that use the base layouts, components, and partials to present content. + +### Best Practices + +- Use **inheritance** (`{% extends %}`) to build on base layouts. +- Employ **components** (`{% include %}`) for reusable UI elements to keep the code DRY and facilitate easier updates. + +### Observation Notes +- The macro-options.json files in the header and footer component act as structural guides. They aren't directly used as data passed to the usaFooter function/macro. Instead, these files outline the expected properties and provide a description of their purpose. The `usaFooter` macro component is currently only invoked in the `admin_template`, which will eventually serve as the `base.html` template. This will simplify the approach when we change the footer macros to componenets by eliminating the need to dynamically pass this data from the base.html template. + + + +### Old Layout Templates We Don't Need +- withoutnav_template.html Delete +- main_template.html Delete +- settings_templates.html `withnav_template` can be used to replace `settings_template`. +- settings_nav.html (move to /new/navigation directory) +- main_nav.html (move to /new/navigation directory) +- service_navigation.html (move to /new/navigation directory) +- org_template, could be under it's own directory called /layout/organization +- org_nav.html (move to /new/navigation directory) +- content_template.html Delete diff --git a/app/templates/views/check/ok.html b/app/templates/views/check/ok.html index b795a5971..015cb1ee1 100644 --- a/app/templates/views/check/ok.html +++ b/app/templates/views/check/ok.html @@ -1,6 +1,5 @@ {% extends "withnav_template.html" %} {% from "components/banner.html" import banner_wrapper %} -{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %} {% from "components/page-header.html" import page_header %} {% from "components/components/button/macro.njk" import usaButton %} {% from "components/components/skip-link/macro.njk" import usaSkipLink %} @@ -9,7 +8,7 @@ {% set file_contents_header_id = 'file-preview' %} {% block service_page_title %} - {{ "Preview of {}".format(template.name) }} + {{ "Select delivery time" }} {% endblock %} @@ -19,11 +18,11 @@ {% block maincolumn_content %} - {{ page_header('Preview of {}'.format(template.name)) }} + {{ page_header('Select delivery time') }} {{ template|string }}
-
- {% if not request.args.from_test %} - -

{{ original_file_name }}

- -
- {% call(item, row_number) list_table( - recipients.displayed_rows, - caption=original_file_name, - caption_visible=False, - field_headings=[ - 'Row in file'|safe - ] + recipients.column_headers - ) %} - {% call index_field() %} - - {% if (item.index + 2) == preview_row %} - {{ item.index + 2 }} - {% else %} - {{ item.index + 2 }} - {% endif %} - - {% endcall %} - {% for column in recipients.column_headers %} - {% if item[column].ignore %} - {{ text_field(item[column].data or '', status='default') }} - {% else %} - {{ text_field(item[column].data or '') }} - {% endif %} - {% endfor %} - {% if item[None].data %} - {% for column in item[None].data %} - {{ text_field(column, status='default') }} - {% endfor %} - {% endif %} - {% endcall %} -
- - {% endif %} - - {% if count_of_displayed_recipients < count_of_recipients %} - - {% endif %} {% endblock %} diff --git a/app/templates/views/check/preview.html b/app/templates/views/check/preview.html new file mode 100644 index 000000000..7398a7fd9 --- /dev/null +++ b/app/templates/views/check/preview.html @@ -0,0 +1,77 @@ +{% extends "withnav_template.html" %} +{% from "components/banner.html" import banner_wrapper %} +{% from "components/table.html" import list_table, field, text_field, hidden_field_heading %} +{% from "components/page-header.html" import page_header %} +{% from "components/components/button/macro.njk" import usaButton %} +{% from "components/components/skip-link/macro.njk" import usaSkipLink %} +{% from "components/components/back-link/macro.njk" import usaBackLink %} + +{% set file_contents_header_id = 'file-preview' %} + +{% block service_page_title %} + {{ "Preview of {}".format(template.name) }} +{% endblock %} + + +{% block backLink %} + {{ usaBackLink({ "href": back_link_from_preview }) }} +{% endblock %} + +{% block maincolumn_content %} + + {{ page_header('Preview') }} +
+

Scheduled: {{ scheduled_for if scheduled_for else 'Now'}}

+

File: {{original_file_name}}

+

Template: {{template.name}}

+

From: {{ template.sender }}

+
+ +

Message

+
{{ simplifed_template|string }}
+ {% if not request.args.from_test %} +

Recipients list

+
+
    +
  • + Description Icon +
    +

    {{ original_file_name }}

    +
    +
  • +
+
+
+ {% call(item, row_number) list_table( + recipients.displayed_rows, + caption="Note: Only the first 5 rows are displayed here.", + caption_visible=True, + field_headings=recipients.column_headers + ) %} + {% for column in recipients.column_headers %} + {% if item[column].ignore %} + {{ text_field(item[column].data or '', status='default') }} + {% else %} + {{ text_field(item[column].data or '') }} + {% endif %} + {% endfor %} + {% if item[None].data %} + {% for column in item[None].data %} + {{ text_field(column, status='default') }} + {% endfor %} + {% endif %} + {% endcall %} +
+ {% endif %} + + +{% endblock %} diff --git a/app/templates/views/notifications/check.html b/app/templates/views/notifications/check.html index 5bc970f9b..8646d1b7f 100644 --- a/app/templates/views/notifications/check.html +++ b/app/templates/views/notifications/check.html @@ -5,7 +5,7 @@ {% from "components/components/button/macro.njk" import usaButton %} {% block service_page_title %} - {{ "Error" if error else "Preview of ‘{}’".format(template.name) }} + {{ "Error" if error else "Select delivery time" }} {% endblock %} {% block backLink %} @@ -40,17 +40,16 @@ {% endcall %}
{% else %} - {{ page_header('Preview of ‘{}’'.format(template.name)) }} + {{ page_header('Select delivery time') }} {% endif %} {{ template|string }}
diff --git a/app/templates/views/notifications/preview.html b/app/templates/views/notifications/preview.html new file mode 100644 index 000000000..a22fda5bb --- /dev/null +++ b/app/templates/views/notifications/preview.html @@ -0,0 +1,74 @@ +{% extends "withnav_template.html" %} +{% from "components/banner.html" import banner_wrapper %} +{% from "components/page-header.html" import page_header %} +{% from "components/components/back-link/macro.njk" import usaBackLink %} +{% from "components/components/button/macro.njk" import usaButton %} + +{% block service_page_title %} + {{ "Error" if error else "Preview" }} +{% endblock %} + +{% block backLink %} + {{ usaBackLink({ "href": back_link_from_preview }) }} +{% endblock %} + +{% block maincolumn_content %} + {% if error == 'not-allowed-to-send-to' %} +
+ {% call banner_wrapper(type='dangerous') %} + {% with + count_of_recipients=1, + template_type_label=( + 'phone number' if template.template_type == 'sms' else 'email address' + ) + %} + {% include "partials/check/not-allowed-to-send-to.html" %} + {% endwith %} + {% endcall %} +
+ {% elif error == 'too-many-messages' %} +
+ {% call banner_wrapper(type='dangerous') %} + {% include "partials/check/too-many-messages.html" %} + {% endcall %} +
+ {% elif error == 'message-too-long' %} + {# the only row_errors we can get when sending one off messages is that the message is too long #} +
+ {% call banner_wrapper(type='dangerous') %} + {% include "partials/check/message-too-long.html" %} + {% endcall %} +
+ {% else %} + {{ page_header('Preview') }} + {% endif %} +
+

Scheduled: {{ scheduled_for if scheduled_for else 'Now'}}

+

Template: {{template.name}}

+

From: {{ template.sender }}

+

To: {{ recipient }}

+
+ +

Message

+
{{ simplifed_template|string }}
+ +
+ +
+ +{% endblock %} diff --git a/tests/app/main/views/test_conversation.py b/tests/app/main/views/test_conversation.py index 215a8e828..1139cd43f 100644 --- a/tests/app/main/views/test_conversation.py +++ b/tests/app/main/views/test_conversation.py @@ -391,7 +391,7 @@ def test_conversation_reply_redirects_with_phone_number_from_notification( ) for element, expected_text in [ - ("h1", "Preview of ‘Two week reminder’"), + ("h1", "Select delivery time"), (".sms-message-recipient", "To: 2021234567"), ( ".sms-message-wrapper", diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index 7448c44cf..35309f57f 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -908,26 +908,22 @@ def test_upload_valid_csv_redirects_to_check_page( ( "extra_args", "expected_link_in_first_row", - "expected_recipient", "expected_message", ), [ ( {}, None, - "To: 2028675301", "Test Service: A, Template content with & entity", ), ( {"row_index": 2}, None, - "To: 2028675301", "Test Service: A, Template content with & entity", ), ( {"row_index": 4}, True, - "To: 2028675303", "Test Service: C, Template content with & entity", ), ], @@ -946,7 +942,6 @@ def test_upload_valid_csv_shows_preview_and_table( fake_uuid, extra_args, expected_link_in_first_row, - expected_recipient, expected_message, ): with client_request.session_transaction() as session: @@ -962,40 +957,20 @@ def test_upload_valid_csv_shows_preview_and_table( """, ) - page = client_request.get( - "main.check_messages", + page = client_request.post( + "main.preview_job", service_id=SERVICE_ONE_ID, template_id=fake_uuid, upload_id=fake_uuid, **extra_args, + _expected_status=200, ) - mock_s3_set_metadata.assert_called_once_with( - SERVICE_ONE_ID, - fake_uuid, - notification_count=3, - template_id=fake_uuid, - valid=True, - original_file_name="example.csv", - ) - - assert page.h1.text.strip() == "Preview of Two week reminder" - assert page.select_one(".sms-message-recipient").text.strip() == expected_recipient + assert page.h1.text.strip() == "Preview" + assert page.select("h2")[1].text.strip() == "Recipients list" + assert page.h2.text.strip() == "Message" assert page.select_one(".sms-message-wrapper").text.strip() == expected_message - - assert page.select_one(".table-field-index").text.strip() == "2" - - if expected_link_in_first_row: - assert page.select_one(".table-field-index a")["href"] == url_for( - "main.check_messages", - service_id=SERVICE_ONE_ID, - template_id=fake_uuid, - upload_id=fake_uuid, - row_index=2, - original_file_name="example.csv", - ) - else: - assert not page.select_one(".table-field-index").select_one("a") + assert not page.select_one(".table-field-index") for row_index, row in enumerate( [ @@ -1043,7 +1018,7 @@ def test_upload_valid_csv_shows_preview_and_table( for index, cell in enumerate(row): row = page.select("table tbody tr")[row_index] assert "id" not in row - assert normalize_spaces(str(row.select("td")[index + 1])) == cell + assert normalize_spaces(str(row.select("td")[index])) == cell def test_show_all_columns_if_there_are_duplicate_recipient_columns( @@ -1674,7 +1649,7 @@ def test_send_one_off_email_to_self_without_placeholders_redirects_to_check_page _follow_redirects=True, ) - assert page.select("h1")[0].text.strip() == "Preview of ‘Two week reminder’" + assert page.select("h1")[0].text.strip() == "Select delivery time" @pytest.mark.parametrize( @@ -1901,10 +1876,9 @@ def test_upload_csvfile_with_valid_phone_shows_all_numbers( original_file_name="example.csv", ) - assert "202 867 0701" in page.text - assert "202 867 0749" in page.text + assert "Select delivery time" in page.text + assert "202 867 0749" not in page.text assert "202 867 0750" not in page.text - assert "Only showing the first 50 rows" in page.text mock_get_notification_count.assert_called_with(service_one["id"]) @@ -1988,7 +1962,7 @@ def test_test_message_can_only_be_sent_now( assert 'name="scheduled_for"' not in content -def test_send_button_is_correctly_labelled( +def test_preview_button_is_correctly_labelled( client_request, mocker, mock_get_live_service, @@ -2013,9 +1987,7 @@ def test_send_button_is_correctly_labelled( template_id=fake_uuid, ) - assert normalize_spaces(page.select_one("main [type=submit]").text) == ( - "Send 1,000 text messages" - ) + assert normalize_spaces(page.select_one("main [type=submit]").text) == ("Preview") @pytest.mark.parametrize("when", ["", "2016-08-25T13:04:21.767198"]) @@ -2043,6 +2015,8 @@ def test_create_job_should_call_api( "valid": True, } } + with client_request.session_transaction() as session: + session["scheduled_for"] = when page = client_request.post( "main.start_job", @@ -2593,7 +2567,7 @@ def test_check_notification_redirects_if_session_not_populated( ) -def test_check_notification_shows_preview( +def test_check_notification_shows_scheduler( client_request, service_one, fake_uuid, mock_get_service_template ): with client_request.session_transaction() as session: @@ -2604,7 +2578,7 @@ def test_check_notification_shows_preview( "main.check_notification", service_id=service_one["id"], template_id=fake_uuid ) - assert page.h1.text.strip() == "Preview of ‘Two week reminder’" + assert page.h1.text.strip() == "Select delivery time" assert (page.find_all("a", {"class": "usa-back-link"})[0]["href"]) == url_for( "main.send_one_off_step", service_id=service_one["id"], @@ -2615,6 +2589,41 @@ def test_check_notification_shows_preview( # assert tour not visible assert not page.select(".banner-tour") + # post to send_notification with help=0 to ensure no back link is then shown + assert page.form.attrs["action"] == url_for( + "main.preview_notification", + service_id=service_one["id"], + template_id=fake_uuid, + ) + + assert normalize_spaces(page.select_one("main [type=submit]").text) == ("Preview") + + +@pytest.mark.parametrize("when", ["", "2016-08-25T13:04:21.767198"]) +def test_preview_notification_shows_preview( + client_request, + service_one, + fake_uuid, + mock_get_service_template, + when, +): + with client_request.session_transaction() as session: + session["recipient"] = "15555555555" + session["placeholders"] = {} + + page = client_request.post( + "main.preview_notification", service_id=service_one["id"], template_id=fake_uuid, + _expected_status=200 + ) + assert page.h1.text.strip() == "Preview" + assert (page.find_all("a", {"class": "usa-back-link"})[0]["href"]) == url_for( + "main.check_notification", + service_id=service_one["id"], + template_id=fake_uuid, + ) + # assert tour not visible + assert not page.select(".banner-tour") + # post to send_notification with help=0 to ensure no back link is then shown assert page.form.attrs["action"] == url_for( "main.send_notification", @@ -2872,7 +2881,6 @@ def test_send_notification_shows_email_error_in_trial_mode( @pytest.mark.parametrize( ("endpoint", "extra_args"), [ - ("main.check_messages", {"template_id": uuid4(), "upload_id": uuid4()}), ("main.send_one_off_step", {"template_id": uuid4(), "step_index": 0}), ], ) diff --git a/tests/app/main/views/test_tour.py b/tests/app/main/views/test_tour.py index 1d34a9999..749a8097f 100644 --- a/tests/app/main/views/test_tour.py +++ b/tests/app/main/views/test_tour.py @@ -544,10 +544,9 @@ def test_should_200_for_check_tour_notification( # post to send_notification keeps help argument assert page.form.attrs["action"] == url_for( - "main.send_notification", + "main.preview_notification", service_id=SERVICE_ONE_ID, template_id=fake_uuid, - help="3", ) diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 1f1da2915..5ee810f33 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -139,6 +139,8 @@ EXCLUDED_ENDPOINTS = tuple( "platform_admin_list_complaints", "platform_admin_reports", "platform_admin_splash_page", + "preview_job", + "preview_notification", "pricing", "privacy", "received_text_messages_callback",