diff --git a/.ds.baseline b/.ds.baseline index 56c3afc7df..df4be33a60 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 123, + "line_number": 120, "is_secret": false } ], @@ -555,7 +555,7 @@ "filename": "tests/app/main/views/test_register.py", "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "is_verified": false, - "line_number": 201, + "line_number": 200, "is_secret": false }, { @@ -563,7 +563,7 @@ "filename": "tests/app/main/views/test_register.py", "hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8", "is_verified": false, - "line_number": 274, + "line_number": 273, "is_secret": false } ], @@ -684,5 +684,5 @@ } ] }, - "generated_at": "2024-11-21T23:08:45Z" + "generated_at": "2025-01-13T20:16:58Z" } diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml index db1540fad5..9315b71721 100644 --- a/.github/actions/setup-project/action.yml +++ b/.github/actions/setup-project/action.yml @@ -15,7 +15,7 @@ runs: python-version: "3.12.3" - name: Install poetry shell: bash - run: pip install poetry + run: pip install poetry==1.8.5 - name: Install application dependencies shell: bash run: make bootstrap diff --git a/Makefile b/Makefile index ed9baf3942..9e0eeb46e9 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,6 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e .PHONY: bootstrap bootstrap: generate-version-file ## Set up everything to run the app - poetry self update poetry self add poetry-dotenv-plugin poetry lock --no-update poetry install --sync --no-root diff --git a/app/__init__.py b/app/__init__.py index 6f9f15e830..54248bda01 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -168,20 +168,12 @@ def _csp(config): def create_app(application): - application.config["FEATURE_BEST_PRACTICES_ENABLED"] = ( - os.getenv("FEATURE_BEST_PRACTICES_ENABLED", "false").lower() == "true" - ) - @application.context_processor def inject_feature_flags(): - feature_best_practices_enabled = application.config.get( - "FEATURE_BEST_PRACTICES_ENABLED", False - ) feature_about_page_enabled = application.config.get( "FEATURE_ABOUT_PAGE_ENABLED", False ) return dict( - FEATURE_BEST_PRACTICES_ENABLED=feature_best_practices_enabled, FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled, ) diff --git a/app/assets/javascripts/sidenav.js b/app/assets/javascripts/sidenav.js new file mode 100644 index 0000000000..24c9f2c5b1 --- /dev/null +++ b/app/assets/javascripts/sidenav.js @@ -0,0 +1,57 @@ +document.addEventListener('DOMContentLoaded', () => { + const sidenavItems = document.querySelectorAll('.usa-sidenav__item > .parent-link'); + let lastPath = window.location.pathname; + let debounceTimeout = null; + + sidenavItems.forEach((link) => { + const parentItem = link.parentElement; + const sublist = parentItem.querySelector('.usa-sidenav__sublist'); + const targetHref = link.getAttribute('href'); + + // initialize the menu to open the correct submenu based on the current route + if (window.location.pathname.startsWith(targetHref)) { + parentItem.classList.add('open'); + link.setAttribute('aria-expanded', 'true'); + } + + link.addEventListener('click', (event) => { + const currentPath = window.location.pathname; + + // prevent default behavior only if navigating to the same route + if (currentPath === targetHref) { + event.preventDefault(); + return; + } + + if (sublist && !parentItem.classList.contains('open')) { + // debounce the menu update to avoid flickering + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(() => { + parentItem.classList.add('open'); + link.setAttribute('aria-expanded', 'true'); + }, 50); + } + }); + }); + + // handle browser back/forward navigation + window.addEventListener('popstate', () => { + const currentPath = window.location.pathname; + + // sync menu state + sidenavItems.forEach((link) => { + const parentItem = link.parentElement; + const targetHref = link.getAttribute('href'); + + if (currentPath.startsWith(targetHref)) { + parentItem.classList.add('open'); + link.setAttribute('aria-expanded', 'true'); + } else { + parentItem.classList.remove('open'); + link.setAttribute('aria-expanded', 'false'); + } + }); + + lastPath = currentPath; + }); + }); diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 1eaa0bfb55..0bb1aab7eb 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -198,8 +198,8 @@ td.table-empty-message { word-wrap: break-word; } - border: 1px solid color('gray-cool-10'); - padding: units(2); + // border: 1px solid color('gray-cool-10'); + // padding: units(2); .tick-cross-list-permissions { margin: units(1) 0; @@ -852,21 +852,36 @@ $do-dont-top-bar-width: 1; } } -.linked-content:hover { - cursor: pointer; - transform: scale(1.05); - transition: transform 0.3s ease, background-color 0.3s ease; -} +.linked-card a { + text-decoration: none; + .usa-card__header, .usa-card__media { + @include at-media('tablet') { + padding-top: units(1); + } + } + &:visited { + color: color('ink'); + } + &:focus .usa-card__container { + outline: units(2px) solid color('blue-40v'); + outline-offset: 0.3rem; + } + &:hover .usa-card__container, &:focus .usa-card__container { + border-color: color('blue-60v'); + background: color('blue-60v'); + p, h3 { + color: white; + } + svg { + filter: brightness(0) invert(1); + } + } -li.linked-card:hover > div:first-child:hover { - border-color: #005ea2; -} + &.linked-content:hover, &.linked-content:focus { + cursor: pointer; + transition: transform 0.3s ease, background-color 0.3s ease; + } -li.linked-card:hover h4, -li.linked-card:hover p, -li.linked-card:hover svg, -.best-practices_card_img:hover { - color: #005ea2; } .best-practices_card_img { @@ -876,10 +891,6 @@ li.linked-card:hover svg, margin: 0 auto; } -.best-practices_link { - text-decoration: none; -} - .usa-link--downloadable { position: relative; } @@ -914,17 +925,25 @@ li.linked-card:hover svg, mask-size: 1.75ex 1.75ex; } +nav.nav { + position: sticky; + top: units(3); +} + .usa-sidenav__sublist { display: none; } -.usa-sidenav__item:hover .usa-sidenav__sublist, -.usa-sidenav__item:focus-within .usa-sidenav__sublist { +.usa-sidenav__item.open .usa-sidenav__sublist { display: block; } -.usa-sidenav__item a { - display: block; +.usa-sidenav__sublist .bold { + font-weight: bold; +} + +.usa-sidenav__sublist li[role="menuitem"] { + border-top: 1px solid #dfe1e2; } .icon-list { @@ -957,6 +976,9 @@ li.linked-card:hover svg, .usa-card__container { align-items: center; text-align: center; + border-radius: 4px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); img { margin: units(4) auto 0; width: units(15); @@ -965,9 +987,17 @@ li.linked-card:hover svg, .usa-card__body { margin-bottom: units(2); } + .blue-bar { + background-color: #005eb8; + height: 1.3em; + width: 100%; + margin: 0; + border-radius: 0; + } } } + .contact-us-card { border: 2px solid color("ink"); padding: units(2); diff --git a/app/config.py b/app/config.py index 1462300472..a82c9a1ab1 100644 --- a/app/config.py +++ b/app/config.py @@ -87,9 +87,6 @@ class Config(object): "tts-benefits-studio@gsa.gov", ], } - FEATURE_BEST_PRACTICES_ENABLED = ( - getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true" - ) FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true" diff --git a/app/main/validators.py b/app/main/validators.py index 1dfc97c488..e1292b9598 100644 --- a/app/main/validators.py +++ b/app/main/validators.py @@ -41,14 +41,8 @@ def __call__(self, form, field): if field.data == "": return - from flask import url_for + message = "Enter a public sector email address." - message = """ - Enter a public sector email address or - find out who can use Notify - """.format( - url_for("main.features") - ) if not is_gov_user(field.data.lower()): raise ValidationError(message) diff --git a/app/main/views/index.py b/app/main/views/index.py index 2cc6537c1f..e05e72e870 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -15,8 +15,6 @@ from app.main.views.pricing import CURRENT_SMS_RATE from app.main.views.sub_navigation_dictionaries import ( about_notify_nav, - best_practices_nav, - features_nav, using_notify_nav, ) from app.utils.user import user_is_logged_in @@ -25,11 +23,6 @@ # Hook to check for feature flags @main.before_request def check_feature_flags(): - if request.path.startswith("/guides") 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 ): @@ -40,8 +33,8 @@ def check_feature_flags(): def test_feature_flags(): return jsonify( { - "FEATURE_BEST_PRACTICES_ENABLED": current_app.config[ - "FEATURE_BEST_PRACTICES_ENABLED" + "FEATURE_ABOUT_PAGE_ENABLED": current_app.config[ + "FEATURE_ABOUT_PAGE_ENABLED" ] } ) @@ -111,44 +104,6 @@ def callbacks(): return redirect(url_for("main.documentation"), 301) -# --- Features page set --- # - - -@main.route("/features") -@user_is_logged_in -def features(): - return render_template("views/features.html", navigation_links=features_nav()) - - -@main.route("/features/roadmap", endpoint="roadmap") -@user_is_logged_in -def roadmap(): - return render_template("views/roadmap.html", navigation_links=features_nav()) - - -@main.route("/features/sms") -@user_is_logged_in -def features_sms(): - return render_template( - "views/features/text-messages.html", navigation_links=features_nav() - ) - - -@main.route("/features/security", endpoint="security") -@user_is_logged_in -def security(): - return render_template("views/security.html", navigation_links=features_nav()) - - -@main.route("/features/using_notify") -@user_is_logged_in -def using_notify(): - return ( - render_template("views/using-notify.html", navigation_links=features_nav()), - 410, - ) - - @main.route("/using-notify/delivery-status") @user_is_logged_in def message_status(): @@ -203,78 +158,75 @@ def trial_mode_new(): ) -@main.route("/guides/best-practices") +@main.route("/using-notify/best-practices") @user_is_logged_in def best_practices(): return render_template( "views/guides/best-practices.html", - navigation_links=best_practices_nav(), + navigation_links=using_notify_nav(), ) -@main.route("/guides/clear-goals") +@main.route("/using-notify/best-practices/clear-goals") @user_is_logged_in def clear_goals(): return render_template( "views/guides/clear-goals.html", - navigation_links=best_practices_nav(), + navigation_links=using_notify_nav(), ) -@main.route("/guides/rules-and-regulations") +@main.route("/using-notify/best-practices/rules-and-regulations") @user_is_logged_in def rules_and_regulations(): return render_template( "views/guides/rules-and-regulations.html", - navigation_links=best_practices_nav(), + navigation_links=using_notify_nav(), ) -@main.route("/guides/establish-trust") +@main.route("/using-notify/best-practices/establish-trust") @user_is_logged_in def establish_trust(): return render_template( "views/guides/establish-trust.html", - navigation_links=best_practices_nav(), + navigation_links=using_notify_nav(), ) -@main.route("/guides/write-for-action") +@main.route("/using-notify/best-practices/write-for-action") @user_is_logged_in def write_for_action(): return render_template( "views/guides/write-for-action.html", - navigation_links=best_practices_nav(), + navigation_links=using_notify_nav(), ) -@main.route("/guides/multiple-languages") +@main.route("/using-notify/best-practices/multiple-languages") @user_is_logged_in def multiple_languages(): return render_template( "views/guides/multiple-languages.html", - navigation_links=best_practices_nav(), + navigation_links=using_notify_nav(), ) -@main.route("/guides/benchmark-performance") +@main.route("/using-notify/best-practices/benchmark-performance") @user_is_logged_in def benchmark_performance(): return render_template( "views/guides/benchmark-performance.html", - navigation_links=best_practices_nav(), + navigation_links=using_notify_nav(), ) -@main.route("/guides/using-notify/guidance") +@main.route("/using-notify/guidance") @user_is_logged_in def guidance_index(): return render_template( "views/guidance/index.html", navigation_links=using_notify_nav(), - feature_best_practices_enabled=current_app.config[ - "FEATURE_BEST_PRACTICES_ENABLED" - ], ) @@ -282,6 +234,8 @@ def guidance_index(): def contact(): return render_template( "views/contact.html", + navigation_links=about_notify_nav(), + ) @@ -313,6 +267,8 @@ def why_text_messaging(): def join_notify(): return render_template( "views/join-notify.html", + navigation_links=about_notify_nav(), + ) @@ -343,17 +299,29 @@ def send_files_by_email(): ) +@main.route("/studio") +def studio(): + return render_template( + "views/studio.html", + ) + + +@main.route("/acceptable-use-policy") +def acceptable_use_policy(): + return render_template( + "views/acceptable-use-policy.html", + ) + + # --- Redirects --- # -@main.route("/roadmap", endpoint="old_roadmap") @main.route("/information-security", endpoint="information_security") @main.route("/using_notify", endpoint="old_using_notify") @main.route("/information-risk-management", endpoint="information_risk_management") @main.route("/integration_testing", endpoint="old_integration_testing") def old_page_redirects(): redirects = { - "main.old_roadmap": "main.roadmap", "main.information_security": "main.using_notify", "main.old_using_notify": "main.using_notify", "main.information_risk_management": "main.security", diff --git a/app/main/views/send.py b/app/main/views/send.py index 2b7a66f96c..2b36e57239 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -483,7 +483,7 @@ def send_one_off_step(service_id, template_id, step_index): link_to_upload=( request.endpoint == "main.send_one_off_step" and step_index == 0 ), - errors=form.errors if form.errors else None + errors=form.errors if form.errors else None, ) diff --git a/app/main/views/sub_navigation_dictionaries.py b/app/main/views/sub_navigation_dictionaries.py index 16991297ab..3b2cf84c17 100644 --- a/app/main/views/sub_navigation_dictionaries.py +++ b/app/main/views/sub_navigation_dictionaries.py @@ -1,88 +1,44 @@ -from flask import current_app - - -def features_nav(): - return [ - { - "name": "Features", - "link": "main.features", - "sub_navigation_items": [ - # { - # "name": "Text messages", - # "link": "main.features_sms", - # }, - ], - }, - { - "name": "Roadmap", - "link": "main.roadmap", - }, - { - "name": "Security", - "link": "main.security", - }, - ] - - def using_notify_nav(): 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(): - return [ { "name": "Best Practices", "link": "main.best_practices", - }, - { - "name": "Clear goals", - "link": "main.clear_goals", - }, - { - "name": "Rules and regulations", - "link": "main.rules_and_regulations", - }, - { - "name": "Establish trust", - "link": "main.establish_trust", "sub_navigation_items": [ { - "name": "Get the word out", - "link": "main.establish_trust#get-the-word-out", + "name": "Clear goals", + "link": "main.clear_goals", }, { - "name": "As people receive texts", - "link": "main.establish_trust#as-people-receive-texts", + "name": "Rules and regulations", + "link": "main.rules_and_regulations", + }, + { + "name": "Establish trust", + "link": "main.establish_trust", + }, + { + "name": "Write for action", + "link": "main.write_for_action", + }, + { + "name": "Multiple languages", + "link": "main.multiple_languages", + }, + { + "name": "Benchmark performance", + "link": "main.benchmark_performance", }, ], }, - { - "name": "Write for action", - "link": "main.write_for_action", - }, - { - "name": "Multiple languages", - "link": "main.multiple_languages", - }, - { - "name": "Benchmark performance", - "link": "main.benchmark_performance", - }, + {"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"}, ] + return nav_items + def about_notify_nav(): return [ @@ -93,20 +49,6 @@ def about_notify_nav(): { "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", @@ -115,7 +57,11 @@ def about_notify_nav(): ], }, { - "name": "Contact", + "name": "Join Notify", + "link": "main.join_notify", + }, + { + "name": "Contact us", "link": "main.contact", }, ] diff --git a/app/navigation.py b/app/navigation.py index 271d6848b3..424d03ae3a 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -40,12 +40,6 @@ class HeaderNavigation(Navigation): "support": { "support", }, - "features": { - "features", - "features_sms", - "roadmap", - "security", - }, "best_practices": { "best_practices", "clear_goals", @@ -57,7 +51,6 @@ class HeaderNavigation(Navigation): }, "using_notify": { "get_started", - "using_notify", "pricing", "trial_mode_new", "message_status", diff --git a/app/templates/base.html b/app/templates/base.html index 0348832bd2..1d2778a9ee 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -56,28 +56,19 @@ {% for item in navigation_links %}