diff --git a/.ds.baseline b/.ds.baseline index c64d22627..56c3afc7d 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 125, + "line_number": 123, "is_secret": false } ], @@ -684,5 +684,5 @@ } ] }, - "generated_at": "2024-11-14T15:53:44Z" + "generated_at": "2024-11-21T23:08:45Z" } diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index f002bb3fc..c3ef5dcbb 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -144,6 +144,7 @@ jobs: inputs: requirements.txt ignore-vulns: | PYSEC-2024-60 + PYSEC-2022-43162 - name: Run npm audit run: make npm-audit diff --git a/app/assets/images/alarm.svg b/app/assets/images/alarm.svg new file mode 100644 index 000000000..57e6180bc --- /dev/null +++ b/app/assets/images/alarm.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/alert.svg b/app/assets/images/alert.svg new file mode 100644 index 000000000..d0d516d8f --- /dev/null +++ b/app/assets/images/alert.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/calendar.svg b/app/assets/images/calendar.svg new file mode 100644 index 000000000..9b755e9fd --- /dev/null +++ b/app/assets/images/calendar.svg @@ -0,0 +1 @@ + diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index da5d77bf2..cec48e82b 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -898,7 +898,7 @@ li.linked-card:hover svg, display: block; } -.about-icon-list { +.icon-list { display: flex; width: 24px; height: 24px; @@ -908,10 +908,6 @@ li.linked-card:hover svg, margin-right: 4px; } -.usa-icon-list__content{ - padding-left: 0; -} - .indented-paragraph { margin-left: calc(24px + 4px); margin-top: 4px; diff --git a/app/config.py b/app/config.py index f40b46dea..146230047 100644 --- a/app/config.py +++ b/app/config.py @@ -91,9 +91,7 @@ class Config(object): getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true" ) - FEATURE_ABOUT_PAGE_ENABLED = ( - getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" - ) + FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" def _s3_credentials_from_env(bucket_prefix): diff --git a/app/main/views/index.py b/app/main/views/index.py index 370a88401..49eeae427 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -30,15 +30,13 @@ # Hook to check for feature flags @main.before_request def check_feature_flags(): - if ( - request.path.startswith("/guides/best-practices") - and not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED", False) + if request.path.startswith("/guides/best-practices") and not current_app.config.get( + "FEATURE_BEST_PRACTICES_ENABLED", False ): abort(404) - if ( - request.path.startswith("/about") - and not current_app.config.get("FEATURE_ABOUT_PAGE_ENABLED", False) + if request.path.startswith("/about") and not current_app.config.get( + "FEATURE_ABOUT_PAGE_ENABLED", False ): abort(404) @@ -314,6 +312,22 @@ def about_notify(): ) +@main.route("/about/security") +def about_security(): + return render_template( + "views/about/security.html", + navigation_links=about_notify_nav(), + ) + + +@main.route("/about/why-text-messaging") +def why_text_messaging(): + return render_template( + "views/about/why-text-messaging.html", + navigation_links=about_notify_nav(), + ) + + @main.route("/using-notify/guidance/create-and-send-messages") @user_is_logged_in def create_and_send_messages(): diff --git a/app/main/views/sub_navigation_dictionaries.py b/app/main/views/sub_navigation_dictionaries.py index b9fb7f8ae..603094cd1 100644 --- a/app/main/views/sub_navigation_dictionaries.py +++ b/app/main/views/sub_navigation_dictionaries.py @@ -1,3 +1,6 @@ +from flask import current_app + + def features_nav(): return [ { @@ -22,46 +25,20 @@ def features_nav(): def using_notify_nav(): - return [ - { - "name": "Get started", - "link": "main.get_started", - }, - { - "name": "Guides", - "link": "main.best_practices", - }, - { - "name": "Trial mode", - "link": "main.trial_mode_new", - }, - { - "name": "Tracking usage", - "link": "main.pricing", - }, - { - "name": "Delivery status", - "link": "main.message_status", - }, - { - "name": "Guidance", - "link": "main.guidance_index", - # "sub_navigation_items": [ - # { - # "name": "Formatting", - # "link": "main.edit_and_format_messages", - # }, - # { - # "name": "Send files by email", - # "link": "main.send_files_by_email", - # }, - # ] - # { - # "name": "API documentation", - # "link": "main.documentation", - # }, - }, + nav_items = [ + {"name": "Get started", "link": "main.get_started"}, + {"name": "Guides", "link": "main.best_practices"}, + {"name": "Trial mode", "link": "main.trial_mode_new"}, + {"name": "Tracking usage", "link": "main.pricing"}, + {"name": "Delivery Status", "link": "main.message_status"}, + {"name": "Guidance", "link": "main.guidance_index"}, ] + if not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED"): + nav_items = [ + item for item in nav_items if item["link"] != "main.best_practices" + ] + + return nav_items def best_practices_nav(): @@ -110,7 +87,31 @@ def best_practices_nav(): def about_notify_nav(): return [ { - "name": "About notify", + "name": "About Notify", "link": "main.about_notify", + "sub_navigation_items": [ + { + "name": "Why text messaging", + "link": "main.why_text_messaging", + "sub_sub_navigation_items": [ + { + "name": "Reach people using a common method", + "link": "main.why_text_messaging#reach-people-using-a-common-method", + }, + { + "name": "Improve customer experience", + "link": "main.why_text_messaging#improve-customer-experience", + }, + { + "name": "What texting is best for", + "link": "main.why_text_messaging#what-texting-is-best-for", + }, + ], + }, + { + "name": "Security", + "link": "main.about_security", + }, + ], }, ] diff --git a/app/navigation.py b/app/navigation.py index a02df484d..271d6848b 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -53,7 +53,7 @@ class HeaderNavigation(Navigation): "establish_trust", "write_for_action", "multiple_languages", - "benchmark_performance" + "benchmark_performance", }, "using_notify": { "get_started", diff --git a/app/templates/base.html b/app/templates/base.html index 369e62b81..b2b6639e9 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -66,7 +66,20 @@ diff --git a/app/templates/views/about/about.html b/app/templates/views/about/about.html index 39dfce671..cd52709c2 100644 --- a/app/templates/views/about/about.html +++ b/app/templates/views/about/about.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% set page_title = "About notify" %} +{% set page_title = "About Notify" %} {% block per_page_title %} {{page_title}} @@ -17,8 +17,9 @@

{{page_title}}

  • Meet people where they are
  • More effectively deliver program outcomes
  • Save administrative costs
  • -
  • Implement 21st Century IDEA and other directives
  • +
  • Implement 21st Century + IDEA and other directives
  • Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration โ€” users can create an account and get started within minutes. We take the security and privacy of messaging data seriously @@ -56,9 +57,9 @@

    Product Highlights

    -

    See if Notify is right for you

    -

    Notify.gov is a product of the Public Benefits Studio, a product accelerator inside +

    See if Notify is right for you

    +

    Notify.gov is a product of the Public Benefits Studio, a product accelerator inside the federal government.

    - - {% endblock %} diff --git a/app/templates/views/about/security.html b/app/templates/views/about/security.html new file mode 100644 index 000000000..9ebc0420f --- /dev/null +++ b/app/templates/views/about/security.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% set page_title = "Security" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} + +
    +

    {{page_title}}

    +

    Notify.gov is built for the needs of government agencies with fundamental system + security processes in place to: +

    + +

    + Notify.gov operates under a full three-year Authority-to-Operate (ATO). This + federal security authorization process leverages security + controls provided by National Institute of Standards and Technology (NIST). +

    + +

    + Our infrastructure runs on cloud.gov and utilizes several + services through Amazon Web + Services (AWS), including + AWS SNS for sending SMS + messages. +

    +

    For more information about the Notify.gov infrastructure, contact us at notify-support@gsa.gov.

    +

    Data

    +

    + On Notify.gov, data is encrypted both in transit and at rest. To send a message, agencies upload a spreadsheet of + phone numbers and other necessary data from their existing data management system. +

    +

    + Notify.gov is not a system of record, so it does not have a System of Records Notice (SORN). Agencies are + responsible for managing their data outside of Notify.gov. +

    +

    Data retention

    +

    + Any data uploads that have recipient data are held for seven calendar days; personally identifiable information + (PII) is never stored in Notifyโ€™s database. +

    +

    Multi-Factor Authentication

    +

    + Notify.gov uses Login.gov for enhanced security. + Login.gov is an extra layer of security created by the government that uses multi-factor authentication and stronger + passwords to protect your account. +

    +

    + To access Notify.gov, users will use a Login.gov account associated with their agency (.gov) email with one of the + multi-factor authentication + methods offered through Login.gov. +

    +
    +{% endblock %} diff --git a/app/templates/views/about/why-text-messaging.html b/app/templates/views/about/why-text-messaging.html new file mode 100644 index 000000000..6aa0887cb --- /dev/null +++ b/app/templates/views/about/why-text-messaging.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} +{% set page_title = "Why text messaging" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} + + +
    +

    {{page_title}}

    +

    Reach people using a common method

    +

    + Confusing or unreceived notifications are one of the largest barriers to people getting and keeping + benefits. The typical ways the government communicates with people often fall short. Low income households are more + likely to experience housing instability, which means paper mail, already slow, can easily be missed. +

    + +

    + Pew Research shows that nearly all adults in the US have a cell phone. Reliance on smartphones + for online access is especially common among Americans with lower household incomes and those with lower levels of + formal education. Of those earning less than $30,000 a year, 28% say their mobile phone is the sole method to + digitally connect. +

    +

    + This means that for many people who rely on government services, cell phones may be the most reliable place to meet + people where they already are. +

    +

    Improve customer experience

    +

    + Text messages can deliver concise information and drive an audience to take action quickly. Timely reminders sent + via text message have been proven to decrease re-enrollment churn and save money for administering agencies. +

    +

    + Texting not only helps programs reach people using a nearly-universal communication method, it is a cost effective + way to do so. With Notify.gov you can get started for free, allowing you to try out + texting to complement your existing communications and outreach strategies. +

    +

    What texting is best for

    +

    + Agencies, like you, are already using Notify.gov to text about the following programs. +

    + {% set card_contents = [ + { + "image_src": asset_url('images/calendar.svg'), + "card_heading": "Reminders", + "p_text": "In a text bubble // Your Quality Control food phone interview is on ((date)) at ((time)). Failure to + attend may lead to closure of your benefits. Call 1-800-222-3333 with questions.", + "alt_text": "reminder text example" + }, + { + "image_src": asset_url('images/alert.svg'), + "card_heading": "Alerts to take action", + "p_text": "In a text bubble // Your household's Medicaid coverage is expiring. To keep getting Medicaid, you must + complete your renewal by ((date)). You can renew online at dhs.state.govโ€ฆ", + "alt_text": "alerts text example" + }, + { + "image_src": asset_url('images/alarm.svg'), + "card_heading": "Important status updates", + "p_text": "In a text bubble // Your passport has been issued at the Los Angeles Passport Agency. Please come to the + desk between 1:30pm and 2:30pm today to pick up your passportโ€ฆ", + "alt_text": "status update text example" + }, + ] %} + {% for item in card_contents %} +
    +
    +
    +

    {{item.card_heading}}

    +

    {{item.p_text}}

    +
    + {% if item.image_src %} + {{ item.alt_text }} + {% endif %} +
    +
    + {% endfor %} +
    +{% endblock %} diff --git a/app/utils/csv.py b/app/utils/csv.py index 4ed6d16b5..79e3535c8 100644 --- a/app/utils/csv.py +++ b/app/utils/csv.py @@ -103,6 +103,7 @@ def generate_notifications_csv(**kwargs): "Carrier Response", "Status", "Time", + "Carrier", ] for header in original_column_headers: if header.lower() != "phone number": @@ -118,6 +119,7 @@ def generate_notifications_csv(**kwargs): "Carrier Response", "Status", "Time", + "Carrier", ] yield ",".join(fieldnames) + "\n" @@ -140,6 +142,7 @@ def generate_notifications_csv(**kwargs): notification["provider_response"], notification["status"], preferred_tz_created_at, + notification["carrier"], ] for header in original_column_headers: if header.lower() != "phone number": @@ -158,6 +161,7 @@ def generate_notifications_csv(**kwargs): notification["provider_response"], notification["status"], preferred_tz_created_at, + notification["carrier"], ] yield Spreadsheet.from_rows([map(str, values)]).as_csv_data diff --git a/deploy-config/production.yml b/deploy-config/production.yml index 42a681cb4..9f5cffc89 100644 --- a/deploy-config/production.yml +++ b/deploy-config/production.yml @@ -1,6 +1,6 @@ env: production instances: 2 -memory: 1.5G +memory: 2G command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application public_admin_route: beta.notify.gov cloud_dot_gov_route: notify.app.cloud.gov diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index af7b7a158..f469f2586 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -18,6 +18,7 @@ Navigation.get_endpoint_with_blueprint, { "about_notify", + "about_security", "accept_invite", "accept_org_invite", "accessibility_statement", @@ -258,6 +259,7 @@ "view_template_version", "view_template_versions", "who_its_for", + "why_text_messaging", "write_for_action", }, ) diff --git a/tests/app/utils/test_csv.py b/tests/app/utils/test_csv.py index d603fcd0e..db4b6a0ec 100644 --- a/tests/app/utils/test_csv.py +++ b/tests/app/utils/test_csv.py @@ -58,6 +58,7 @@ def _get( "to": recipient, "recipient": recipient, "client_reference": "ref 1234", + "carrier": "AT&T Mobility", } for i in range(rows) ], @@ -88,15 +89,15 @@ def get_notifications_csv_mock( ( None, [ - "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time\n", - "8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", + "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n", + "8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n", ], ), ( "Anne Example", [ - "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time\n", - "8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", # noqa + "Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n", + "8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n", # noqa ], ), ], @@ -135,6 +136,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", ], [ "8005555555", @@ -144,6 +146,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", ], ), ( @@ -159,6 +162,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", "a", "b", "c", @@ -171,6 +175,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", "๐Ÿœ", "๐Ÿ", "๐Ÿฆ€", @@ -189,6 +194,7 @@ def test_generate_notifications_csv_without_job( "Carrier Response", "Status", "Time", + "Carrier", "a", "b", "c", @@ -201,6 +207,7 @@ def test_generate_notifications_csv_without_job( "Did not like it", "Delivered", "1943-04-19 08:00:00 AM US/Eastern", + "AT&T Mobility", "๐Ÿœ,๐Ÿœ", "๐Ÿ,๐Ÿ", "๐Ÿฆ€", diff --git a/tests/end_to_end/test_best_practices_content_pages.py b/tests/end_to_end/test_best_practices_content_pages.py index 962100de3..031e4baef 100644 --- a/tests/end_to_end/test_best_practices_content_pages.py +++ b/tests/end_to_end/test_best_practices_content_pages.py @@ -11,7 +11,7 @@ def test_best_practices_side_menu(authenticated_page): page = authenticated_page - page.goto(f"{E2E_TEST_URI}/best-practices") + page.goto(f"{E2E_TEST_URI}/guides/best-practices") page.wait_for_load_state("domcontentloaded") check_axe_report(page) @@ -58,7 +58,7 @@ def test_best_practices_side_menu(authenticated_page): def test_breadcrumbs_best_practices(authenticated_page): page = authenticated_page - page.goto(f"{E2E_TEST_URI}/best-practices") + page.goto(f"{E2E_TEST_URI}/guides/best-practices") page.wait_for_load_state("domcontentloaded") check_axe_report(page) diff --git a/urls.js b/urls.js index 0440430ec..2279b5a79 100644 --- a/urls.js +++ b/urls.js @@ -8,7 +8,7 @@ const sublinks = [ { label: 'Trial Mode', path: '/using-notify/trial-mode' }, { label: 'Pricing', path: '/using-notify/pricing' }, { label: 'Delivery Status', path: '/using-notify/delivery-status' }, - { label: 'Guidance', path: '/using-notify/guidance' }, + { label: 'Guidance', path: '/guides/using-notify/guidance' }, { label: 'Features', path: '/features' }, { label: 'Roadmap', path: '/features/roadmap' }, { label: 'Security', path: '/features/security' },