diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..0086358db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/issue_template.yml b/.github/ISSUE_TEMPLATE/issue_template.yml new file mode 100644 index 000000000..576af0095 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.yml @@ -0,0 +1,76 @@ +name: "User Story Template" +description: "Use this template for creating user stories" +title: "User Story: [Brief description of the user story]" + +labels: + - "Type: User Story" + +body: + - type: markdown + attributes: + value: '**User Story:**' + - type: textarea + id: userType + attributes: + label: "As a [type of user]," + description: "Describe the type of user involved in this story." + validations: + required: true + - type: textarea + id: actionFeature + attributes: + label: "I want [an action or feature]," + description: "Describe the desired action or feature from the user's perspective." + validations: + required: true + - type: textarea + id: benefitValue + attributes: + label: "So that [benefit or value]." + description: "Describe the benefit or value the user expects from the action or feature." + validations: + required: true + + - type: markdown + attributes: + value: '**Acceptance Criteria:**' + - type: textarea + id: acceptanceCriteria + attributes: + label: "Detailed condition or criteria that must be met for the user story to be considered complete." + description: "List the acceptance criteria for the user story." + validations: + required: true + + - type: markdown + attributes: + value: '**Tasks:**' + - type: textarea + id: tasks + attributes: + label: "List of specific tasks or sub-tasks that need to be done to implement the user story." + description: "Outline the tasks necessary to implement the user story." + validations: + required: true + + - type: markdown + attributes: + value: '**Dependencies:**' + - type: textarea + id: dependencies + attributes: + label: "List any dependencies that need to be resolved before starting or completing this user story." + description: "Specify any dependencies related to the user story." + validations: + required: false + + - type: markdown + attributes: + value: '**Notes:**' + - type: textarea + id: notes + attributes: + label: "Any additional information or context that might be useful for the team." + description: "Provide any extra notes or context for the user story." + validations: + required: false diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index 771209267..c308cd71a 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -18,7 +18,7 @@ jobs: - name: Check for changes to Terraform id: changed-terraform-files - uses: tj-actions/changed-files@v1.1.2 + uses: tj-actions/changed-files@v41.0.0 with: files: | terraform/demo @@ -72,7 +72,7 @@ jobs: - name: Check for changes to egress config id: changed-egress-config - uses: tj-actions/changed-files@v34 + uses: tj-actions/changed-files@v41.0.0 with: files: | deploy-config/egress_proxy/notify-admin-demo.*.acl diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 0f696de36..3c1ab8b8a 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -18,7 +18,7 @@ jobs: - name: Check for changes to Terraform id: changed-terraform-files - uses: tj-actions/changed-files@v1.1.2 + uses: tj-actions/changed-files@v41.0.0 with: files: | terraform/production @@ -73,7 +73,7 @@ jobs: - name: Check for changes to egress config id: changed-egress-config - uses: tj-actions/changed-files@v34 + uses: tj-actions/changed-files@v41.0.0 with: files: | deploy-config/egress_proxy/notify-admin-production.*.acl diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8ef41c2a7..767acae72 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,7 +23,7 @@ jobs: - name: Check for changes to Terraform id: changed-terraform-files - uses: tj-actions/changed-files@v1.1.2 + uses: tj-actions/changed-files@v41.0.0 with: files: | terraform/staging @@ -93,7 +93,7 @@ jobs: - name: Check for changes to egress config id: changed-egress-config - uses: tj-actions/changed-files@v34 + uses: tj-actions/changed-files@v41.0.0 with: files: | deploy-config/egress_proxy/notify-admin-staging.*.acl diff --git a/app/__init__.py b/app/__init__.py index 4e53bbb76..9ec056c2b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -39,6 +39,7 @@ from app.asset_fingerprinter import asset_fingerprinter from app.config import configs from app.extensions import redis_client, zendesk_client from app.formatters import ( + convert_markdown_template, convert_to_boolean, format_auth_type, format_billions, @@ -88,6 +89,7 @@ from app.navigation import ( HeaderNavigation, MainNavigation, OrgNavigation, + SecondaryNavigation, ) from app.notify_client import InviteTokenError from app.notify_client.api_key_api_client import api_key_api_client @@ -128,6 +130,7 @@ navigation = { "main_navigation": MainNavigation(), "header_navigation": HeaderNavigation(), "org_navigation": OrgNavigation(), + "secondary_navigation": SecondaryNavigation(), } @@ -582,6 +585,7 @@ def add_template_filters(application): format_mobile_network, format_yes_no, square_metres_to_square_miles, + convert_markdown_template, ]: application.add_template_filter(fn) diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 341ee6d9a..06bff1ca7 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -22,16 +22,26 @@ i.e. @use "uswds-core" as *; -.usa-header--extended .usa-logo { - font-family: family("sans"); - margin: units(4) 0; - @include at-media-max('mobile-lg') { - margin: units(4) 0 units(4) units(2); +.usa-header--extended { + .usa-logo { + font-family: family("sans"); + margin: units(4) 0; + @include at-media-max('mobile-lg') { + margin: units(4) 0 units(4) units(2); + } + img { + @include at-media($theme-header-min-width) { + width: 80px; + height: 70px; + } + } } - img { - @include at-media($theme-header-min-width) { - width: 80px; - height: 70px; + .usa-nav__secondary { + .usa-nav__link { + padding: 0; + &.usa-current { + text-decoration: underline; + } } } } @@ -44,15 +54,12 @@ i.e. &> .usa-nav__primary-item > a { font-size: size("body", 4); } - &> .usa-nav__primary-item:last-child { - margin-left: auto; - @include u-margin-right(-4); - } + // &> .usa-nav__primary-item:last-child { + // margin-left: auto; + // @include u-margin-right(-4); + // } } -.usa-nav__primary - - h1 { font-weight: bold !important; } @@ -114,12 +121,46 @@ td.table-empty-message { text-decoration: none !important; } +.button-flex-header { + flex-direction: column; + align-items: flex-start; + @include at-media(desktop) { + flex-direction: row; + display: flex; + align-items: center; + justify-content: space-between; + } +} + .user-list-edit-link:active:before, .user-list-edit-link:focus:before { box-shadow: none; border: 0; } +.user-list { + display: inline-flex; + flex-wrap: wrap; + gap: units(2); + .user-list-item { + @include at-media(desktop) { + flex: 1 1 calc(50% - units(2)); + } + flex: 1 1 calc(100% - units(2)); + border: 1px solid color('gray-cool-10'); + padding: units(2); + .tick-cross-list-permissions { + margin: units(1) 0; + padding-left: units(2); + } + .hint { + display: block; + font-size: size("body", "sm"); + font-weight: normal; + } + } +} + .template-list-item-without-ancestors .template-list-folder:active, .template-list-item-without-ancestors .template-list-folder:active::before, .template-list-item-without-ancestors .template-list-folder:focus, @@ -236,7 +277,7 @@ td.table-empty-message { } .navigation-service.usa-breadcrumb { - -bottom: 0; + bottom: 0; } // Dashboard diff --git a/app/content/get-started.md b/app/content/get-started.md new file mode 100644 index 000000000..7846c0fe6 --- /dev/null +++ b/app/content/get-started.md @@ -0,0 +1,43 @@ +# Get started + + +1. ## Check if Notify.gov is right for you +
Read about our features, pricing and roadmap.
+ + +2. ## Create an account + {% if not current_user.is_authenticated %} +Create an account for free and add your first Notify service. When you add a new service it will start in trial mode.
+ {% else %} +Create an account for free and add your first Notify service. When you add a new service, it will start in trial mode.
+ {% endif %} + +3. ## Write some messages + {% if True %} +Add message templates with examples of the content you plan to send. You can use our guidance to help you.
+ {% endif %} + +4. ## Set up your service + {% if not current_user.is_authenticated or not current_service %} +Review your settings to add message branding and sender information.
+Add team members and check their permissions.
+ {% else %} +Review your settings to add message branding and sender information.
+Add team members and check their permissions.
+ {% endif %} + + +5. ## Start sending messages + {% if not current_user.is_authenticated or not current_service %} +When you’re ready to send messages to people outside your team, go to the Settings page and select Request to go live. We’ll approve your request within one working day.
+ {% else %} +You should request to go live when you’re ready to send messages to people outside your team. We’ll approve your request within one working day.
+ {% endif %} + + + + diff --git a/app/formatters.py b/app/formatters.py index 2442e70d0..86c187f55 100644 --- a/app/formatters.py +++ b/app/formatters.py @@ -1,3 +1,4 @@ +import os import re import unicodedata import urllib @@ -9,8 +10,11 @@ from numbers import Number import ago import dateutil import humanize +import markdown import pytz -from flask import Markup, url_for +from bs4 import BeautifulSoup +from flask import Markup, render_template_string, url_for +from flask.helpers import get_root_path from notifications_utils.field import Field from notifications_utils.formatters import make_quotes_smart from notifications_utils.formatters import nl2br as utils_nl2br @@ -21,6 +25,41 @@ from app.utils.csv import get_user_preferred_timezone from app.utils.time import parse_naive_dt +def apply_html_class(tags, html_file): + new_html = html_file + + for tag in tags: + element = tag[0] + class_name = tag[1] + + soup = BeautifulSoup(new_html, "html.parser") + + for xtag in soup.find_all(element): + xtag["class"] = class_name + + new_html = str(soup) + + return new_html + + +def convert_markdown_template(mdf, test=False): + content_text = "" + + if not test: + APP_ROOT = get_root_path("notifications-admin") + file = "app/content/" + mdf + ".md" + md_file = os.path.join(APP_ROOT, file) + with open(md_file) as f: + content_text = f.read() + else: + content_text = mdf + + jn_render = render_template_string(content_text) + md_render = markdown.markdown(jn_render) + + return md_render + + def convert_to_boolean(value): if isinstance(value, str): if value.lower() in ["t", "true", "on", "yes", "1"]: diff --git a/app/main/forms.py b/app/main/forms.py index 2520474fc..d9a35b72a 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1235,15 +1235,20 @@ class ChangeNameForm(StripWhitespaceForm): new_name = GovukTextInputField("Your name") +# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones class ChangePreferredTimezoneForm(StripWhitespaceForm): def __init__(self, *args, **kwargs): super(ChangePreferredTimezoneForm, self).__init__(*args, **kwargs) self.new_preferred_timezone.choices = [ + ("America/Puerto_Rico", "America/Puerto_Rico"), ("US/Eastern", "US/Eastern"), ("US/Central", "US/Central"), ("US/Mountain", "US/Mountain"), ("US/Pacific", "US/Pacific"), + ("US/Alaska", "US/Alaska"), ("US/Hawaii", "US/Hawaii"), + ("US/Aleutian", "US/Aleutian"), + ("US/Samoa", "US/Samoa"), ] new_preferred_timezone = GovukRadiosField( diff --git a/app/main/views/index.py b/app/main/views/index.py index 044543526..f4e3a67db 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -2,6 +2,7 @@ from flask import abort, redirect, render_template, request, url_for from flask_login import current_user from app import status_api_client +from app.formatters import apply_html_class, convert_markdown_template from app.main import main from app.main.views.pricing import CURRENT_SMS_RATE from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav @@ -137,9 +138,20 @@ def get_started_old(): @main.route("/using-notify/get-started") @user_is_logged_in def get_started(): + markdown = convert_markdown_template("get-started") + html_style = apply_html_class( + [ + ["ol", "usa-process-list"], + ["li", "usa-process-list__item padding-bottom-4 margin-top-2"], + ["h2", "usa-process-list__heading line-height-sans-3"], + ], + markdown, + ) + return render_template( "views/get-started.html", navigation_links=using_notify_nav(), + content=html_style, ) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index feb1e4388..1cac1410c 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -124,6 +124,14 @@ def service_switch_live(service_id): if form.validate_on_submit(): current_service.update_status(live=form.enabled.data) + if form.enabled.data is True: + billing_api_client.create_or_update_free_sms_fragment_limit( + service_id, 250000 + ) + else: + billing_api_client.create_or_update_free_sms_fragment_limit( + service_id, 40000 + ) return redirect(url_for(".service_settings", service_id=service_id)) return render_template( diff --git a/app/navigation.py b/app/navigation.py index e824d6de2..94f970540 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -87,39 +87,7 @@ class HeaderNavigation(Navigation): "uploads", "view_job", "view_jobs", - "confirm_edit_user_email", - "confirm_edit_user_mobile_number", - "edit_user_email", - "edit_user_mobile_number", - "edit_user_permissions", - "invite_user", - "manage_users", - "remove_user_from_service", "usage", - "link_service_to_organization", - "service_add_email_reply_to", - "service_add_sms_sender", - "service_confirm_delete_email_reply_to", - "service_confirm_delete_sms_sender", - "service_edit_email_reply_to", - "service_edit_sms_sender", - "service_email_reply_to", - "service_name_change", - "service_set_auth_type", - "service_set_channel", - "send_files_by_email_contact_details", - "service_set_inbound_number", - "service_set_inbound_sms", - "service_set_international_sms", - "service_set_reply_to_email", - "service_set_sms_prefix", - "service_verify_reply_to_address", - "service_verify_reply_to_address_updates", - "service_settings", - "service_sms_senders", - "set_free_sms_allowance", - "set_message_limit", - "set_rate_limit", }, "pricing": { "how_to_pay", @@ -236,6 +204,21 @@ class MainNavigation(Navigation): "usage": { "usage", }, + "user-profile": { + "user_profile", + "user_profile_confirm_delete_mobile_number", + "user_profile_email", + "user_profile_email_authenticate", + "user_profile_email_confirm", + "user_profile_mobile_number", + "user_profile_mobile_number_authenticate", + "user_profile_mobile_number_confirm", + "user_profile_mobile_number_delete", + "user_profile_name", + "user_profile_password", + "user_profile_preferred_timezone", + "user_profile_disable_platform_admin_view", + }, "settings": { "link_service_to_organization", "service_add_email_reply_to", @@ -297,6 +280,58 @@ class CaseworkNavigation(Navigation): } +class SecondaryNavigation(Navigation): + mapping = { + "settings": { + "link_service_to_organization", + "service_add_email_reply_to", + "service_add_sms_sender", + "service_confirm_delete_email_reply_to", + "service_confirm_delete_sms_sender", + "service_edit_email_reply_to", + "service_edit_sms_sender", + "service_email_reply_to", + "service_name_change", + "service_set_auth_type", + "service_set_channel", + "send_files_by_email_contact_details", + "service_set_inbound_number", + "service_set_inbound_sms", + "service_set_international_sms", + "service_set_reply_to_email", + "service_set_sms_prefix", + "service_verify_reply_to_address", + "service_verify_reply_to_address_updates", + "service_settings", + "service_sms_senders", + "set_free_sms_allowance", + "set_message_limit", + "set_rate_limit", + "confirm_edit_user_email", + "confirm_edit_user_mobile_number", + "edit_user_email", + "edit_user_mobile_number", + "edit_user_permissions", + "invite_user", + "manage_users", + "remove_user_from_service", + "user_profile", + "user_profile_confirm_delete_mobile_number", + "user_profile_email", + "user_profile_email_authenticate", + "user_profile_email_confirm", + "user_profile_mobile_number", + "user_profile_mobile_number_authenticate", + "user_profile_mobile_number_confirm", + "user_profile_mobile_number_delete", + "user_profile_name", + "user_profile_password", + "user_profile_preferred_timezone", + "user_profile_disable_platform_admin_view", + }, + } + + class OrgNavigation(Navigation): mapping = { "dashboard": { diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index 86482f829..c48f768ce 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -58,26 +58,37 @@ "text": "Features", "active": header_navigation.is_selected('features') }, - { - "href": url_for('main.user_profile'), - "text": current_user.name, - "active": header_navigation.is_selected('user-profile') - }, { "href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin') }, - { - "href": url_for('main.sign_out'), - "text": "Sign out" - }, { "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 = [ { @@ -104,13 +115,29 @@ "href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile') - }, - { - "href": url_for('main.sign_out'), - "text": "Sign out" } ] %} - {% endif %} + {% 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 = [ @@ -142,6 +169,7 @@ "productName": "Notify", "navigation": navigation, "navigationClasses": "govuk-header__navigation--end", + "secondaryNavigation": secondaryNavigation, "assetsPath": asset_path + "images" }) }} {% endblock %} @@ -236,7 +264,7 @@ 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. +
You have been inactive for too long. Your session will expire in .
Create an account for free and add your first Notify service. When you add a new service it will start in trial mode.
- {% else %} -Create an account for free and add your first Notify service. When you add a new service, it will start in trial mode.
- {% endif %} -Add message templates with examples of the content you plan to send. You can use our guidance to help you.
-Review your settings to add message customization and sender information.
-Add team members and check their permissions.
- {% else %} -Review your settings to add message customization and sender information.
-Add team members and check their permissions.
- {% endif %} -When you’re ready to send messages to people outside your team, go to the Settings page and select Request to go live. We’ll approve your request within one working day.
- {% else %} -You should request to go live when you’re ready to send messages to people outside your team. We’ll approve your request within one working day.
- {% endif %} - -