Pull main

This commit is contained in:
Andrew Shumway
2024-05-20 10:25:43 -06:00
207 changed files with 50341 additions and 573 deletions
+1 -8
View File
@@ -71,7 +71,7 @@ too-complex:
.PHONY: py-test
py-test: export NEW_RELIC_ENVIRONMENT=test
py-test: ## Run python unit tests
poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
poetry run coverage run -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
poetry run coverage report --fail-under=96
poetry run coverage html -d .coverage_cache
@@ -101,13 +101,6 @@ py-lock: ## Syncs dependencies and updates lock file without performing recursiv
poetry lock --no-update
poetry install --sync
.PHONY: update-utils
update-utils: ## Forces Poetry to pull the latest changes from the notifications-utils repo; requires that you commit the changes to poetry.lock!
poetry update notifications-utils
@echo
@echo !!! PLEASE MAKE SURE TO COMMIT AND PUSH THE UPDATED poetry.lock FILE !!!
@echo
.PHONY: freeze-requirements
freeze-requirements: ## create static requirements.txt
poetry export --without-hashes --format=requirements.txt > requirements.txt
-18
View File
@@ -19,7 +19,6 @@ UI's backend and is required for most things to function. Set that up first!
Our other repositories are:
- [notifications-admin](https://github.com/GSA/notifications-admin)
- [notifications-utils](https://github.com/GSA/notifications-utils)
- [us-notify-compliance](https://github.com/GSA/us-notify-compliance/)
- [notify-python-demo](https://github.com/GSA/notify-python-demo)
@@ -420,23 +419,6 @@ In either situation, once you are finished and have verified the dependency
changes are working, please be sure to commit both the `pyproject.toml` and
`poetry.lock` files.
### Keeping the notification-utils Dependency Up-to-Date
The `notifications-utils` dependency references the other repository we have at
https://github.com/GSA/notifications-utils - this dependency requires a bit of
extra legwork to ensure it stays up-to-date.
Whenever a PR is merged in the `notifications-utils` repository, we need to make
sure the changes are pulled in here and committed to this repository as well.
You can do this by going through these steps:
- Make sure your local `main` branch is up-to-date
- Create a new branch to work in
- Run `make update-utils`
- Commit the updated `poetry.lock` file and push the changes
- Make a new PR with the change
- Have the PR get reviewed and merged
## Known Installation Issues
### Python Installation Errors
+6 -6
View File
@@ -23,12 +23,6 @@ from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
from itsdangerous import BadSignature
from notifications_python_client.errors import HTTPError
from notifications_utils import logging, request_helper
from notifications_utils.formatters import (
formatted_list,
get_lines_with_normalised_whitespace,
)
from notifications_utils.recipients import format_phone_number_human_readable
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from werkzeug.exceptions import abort
from werkzeug.local import LocalProxy
@@ -114,6 +108,12 @@ from app.notify_client.upload_api_client import upload_api_client
from app.notify_client.user_api_client import user_api_client
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
from notifications_utils import logging, request_helper
from notifications_utils.formatters import (
formatted_list,
get_lines_with_normalised_whitespace,
)
from notifications_utils.recipients import format_phone_number_human_readable
login_manager = LoginManager()
csrf = CSRFProtect()
+24
View File
@@ -0,0 +1,24 @@
(function (window) {
const ctx = document.getElementById('myChart');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
})(window);
+1 -1
View File
@@ -2,9 +2,9 @@ import json
from os import getenv
import newrelic.agent
from notifications_utils import DAILY_MESSAGE_LIMIT
from app.cloudfoundry_config import cloud_config
from notifications_utils import DAILY_MESSAGE_LIMIT
class Config(object):
+3 -3
View File
@@ -16,15 +16,15 @@ from bs4 import BeautifulSoup
from flask import render_template_string, url_for
from flask.helpers import get_root_path
from markupsafe import Markup
from app.utils.csv import get_user_preferred_timezone
from app.utils.time import parse_naive_dt
from notifications_utils.field import Field
from notifications_utils.formatters import make_quotes_smart
from notifications_utils.formatters import nl2br as utils_nl2br
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
from notifications_utils.take import Take
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
+3 -3
View File
@@ -11,9 +11,6 @@ from flask_wtf.file import FileAllowed
from flask_wtf.file import FileField as FileField_wtf
from flask_wtf.file import FileSize
from markupsafe import Markup
from notifications_utils.formatters import strip_all_whitespace
from notifications_utils.insensitive_dict import InsensitiveDict
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
from werkzeug.utils import cached_property
from wtforms import (
BooleanField,
@@ -66,6 +63,9 @@ from app.models.organization import Organization
from app.utils import merge_jsonlike
from app.utils.csv import get_user_preferred_timezone
from app.utils.user_permissions import all_ui_permissions, permission_options
from notifications_utils.formatters import strip_all_whitespace
from notifications_utils.insensitive_dict import InsensitiveDict
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
def get_time_value_and_label(future_time):
+4 -4
View File
@@ -1,15 +1,15 @@
import re
from abc import ABC, abstractmethod
from notifications_utils.field import Field
from notifications_utils.formatters import formatted_list
from notifications_utils.recipients import InvalidEmailError, validate_email_address
from notifications_utils.sanitise_text import SanitiseSMS
from wtforms import ValidationError
from app.main._commonly_used_passwords import commonly_used_passwords
from app.models.spreadsheet import Spreadsheet
from app.utils.user import is_gov_user
from notifications_utils.field import Field
from notifications_utils.formatters import formatted_list
from notifications_utils.recipients import InvalidEmailError, validate_email_address
from notifications_utils.sanitise_text import SanitiseSMS
class CommonlyUsedPassword:
+2 -2
View File
@@ -1,14 +1,14 @@
from flask import jsonify, redirect, render_template, session, url_for
from flask_login import current_user
from notifications_python_client.errors import HTTPError
from notifications_utils.recipients import format_phone_number_human_readable
from notifications_utils.template import SMSPreviewTemplate
from app import current_service, notification_api_client, service_api_client
from app.main import main
from app.main.forms import SearchByNameForm
from app.models.template_list import TemplateList
from app.utils.user import user_has_permissions
from notifications_utils.recipients import format_phone_number_human_readable
from notifications_utils.template import SMSPreviewTemplate
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>")
+1 -1
View File
@@ -6,7 +6,6 @@ from itertools import groupby
from flask import Response, abort, jsonify, render_template, request, session, url_for
from flask_login import current_user
from notifications_utils.recipients import format_phone_number_human_readable
from werkzeug.utils import redirect
from app import (
@@ -30,6 +29,7 @@ from app.utils.csv import Spreadsheet
from app.utils.pagination import generate_next_dict, generate_previous_dict
from app.utils.time import get_current_financial_year
from app.utils.user import user_has_permissions
from notifications_utils.recipients import format_phone_number_human_readable
@main.route("/services/<uuid:service_id>/dashboard")
+1 -1
View File
@@ -15,7 +15,6 @@ from flask import (
)
from flask_login import current_user
from markupsafe import Markup
from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTemplate
from app import (
current_service,
@@ -35,6 +34,7 @@ from app.utils.pagination import (
get_page_from_request,
)
from app.utils.user import user_has_permissions
from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTemplate
@main.route("/services/<uuid:service_id>/jobs")
+1 -1
View File
@@ -10,12 +10,12 @@ from flask import (
url_for,
)
from itsdangerous import SignatureExpired
from notifications_utils.url_safe_token import check_token
from app.main import main
from app.main.forms import NewPasswordForm
from app.models.user import User
from app.utils.login import log_in_user
from notifications_utils.url_safe_token import check_token
@main.route("/new-password/<path:token>", methods=["GET", "POST"])
+1 -1
View File
@@ -1,11 +1,11 @@
from flask import current_app, render_template
from flask_login import current_user
from notifications_utils.international_billing_rates import INTERNATIONAL_BILLING_RATES
from app.main import main
from app.main.forms import SearchByNameForm
from app.main.views.sub_navigation_dictionaries import using_notify_nav
from app.utils.user import user_is_logged_in
from notifications_utils.international_billing_rates import INTERNATIONAL_BILLING_RATES
CURRENT_SMS_RATE = "1.72"
+107 -60
View File
@@ -15,7 +15,7 @@ from flask import (
)
from flask_login import current_user
from app import user_api_client
from app import redis_client, user_api_client
from app.main import main
from app.main.forms import (
RegisterUserForm,
@@ -113,74 +113,89 @@ def registration_continue():
raise Exception("Unexpected routing in registration_continue")
def get_invite_data_from_redis(state):
invite_data = json.loads(redis_client.raw_get(f"invitedata-{state}"))
user_email = redis_client.raw_get(f"user_email-{state}").decode("utf8")
user_uuid = redis_client.raw_get(f"user_uuid-{state}").decode("utf8")
invited_user_email_address = redis_client.raw_get(
f"invited_user_email_address-{state}"
).decode("utf8")
return invite_data, user_email, user_uuid, invited_user_email_address
def put_invite_data_in_redis(
state, invite_data, user_email, user_uuid, invited_user_email_address
):
ttl = 60 * 15 # 15 minutes
redis_client.raw_set(f"invitedata-{state}", json.dumps(invite_data), ex=ttl)
redis_client.raw_set(f"user_email-{state}", user_email, ex=ttl)
redis_client.raw_set(f"user_uuid-{state}", user_uuid, ex=ttl)
redis_client.raw_set(
f"invited_user_email_address-{state}",
invited_user_email_address,
ex=ttl,
)
def check_invited_user_email_address_matches_expected(
user_email, invited_user_email_address
):
if user_email.lower() != invited_user_email_address.lower():
debug_msg("invited user email did not match expected email, abort(403)")
flash("You cannot accept an invite for another person.")
abort(403)
@main.route("/set-up-your-profile", methods=["GET", "POST"])
@hide_from_search_engines
def set_up_your_profile():
form = SetupUserProfileForm()
debug_msg(f"Enter set_up_your_profile with request.args {request.args}")
code = request.args.get("code")
state = request.args.get("state")
login_gov_error = request.args.get("error")
if form.validate_on_submit():
# start login.gov
code = request.args.get("code")
state = request.args.get("state")
login_gov_error = request.args.get("error")
if code and state:
return _handle_login_dot_gov_invite(code, state, form)
elif login_gov_error:
current_app.logger.error(f"login.gov error: {login_gov_error}")
raise Exception(f"Could not login with login.gov {login_gov_error}")
# end login.gov
if redis_client.raw_get(f"invitedata-{state}") is None:
access_token = sign_in._get_access_token(code, state)
debug_msg("Got the access token for login.gov")
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
debug_msg(
f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov"
)
invite_data = state.encode("utf8")
invite_data = base64.b64decode(invite_data)
invite_data = json.loads(invite_data)
debug_msg(f"final state {invite_data}")
invited_user_id = invite_data["invited_user_id"]
invited_user_email_address = get_invited_user_email_address(invited_user_id)
debug_msg(f"email address from the invite_date is {invited_user_email_address}")
check_invited_user_email_address_matches_expected(
user_email, invited_user_email_address
)
return render_template("views/set-up-your-profile.html", form=form)
def get_invited_user_email_address(invited_user_id):
# InvitedUser is an unhashable type and hard to mock in tests
# so this convenience method is a workaround for that
invited_user = InvitedUser.by_id(invited_user_id)
return invited_user.email_address
def invited_user_accept_invite(invited_user_id):
# InvitedUser is an unhashable type and hard to mock in tests
# so this convenience method is a workaround for that
invited_user = InvitedUser.by_id(invited_user_id)
invited_user.accept_invite()
def debug_msg(msg):
current_app.logger.debug(hilite(msg))
def _handle_login_dot_gov_invite(code, state, form):
debug_msg(f"enter _handle_login_dot_gov_invite with code {code} state {state}")
access_token = sign_in._get_access_token(code, state)
debug_msg("Got the access token for login.gov")
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
debug_msg(
f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov"
)
debug_msg(f"raw state {state}")
invite_data = state.encode("utf8")
debug_msg(f"utf8 encoded state {invite_data}")
invite_data = base64.b64decode(invite_data)
debug_msg(f"b64 decoded state {invite_data}")
invite_data = json.loads(invite_data)
debug_msg(f"final state {invite_data}")
invited_user_id = invite_data["invited_user_id"]
invited_user_email_address = get_invited_user_email_address(invited_user_id)
debug_msg(f"email address from the invite_date is {invited_user_email_address}")
if user_email.lower() != invited_user_email_address.lower():
debug_msg("invited user email did not match expected email, abort(403)")
flash("You cannot accept an invite for another person.")
session.pop("invited_user_id", None)
abort(403)
else:
invited_user_accept_invite(invited_user_id)
debug_msg(
f"invited user {invited_user_email_address} to service {invite_data['service_id']}"
f"accepted invite user {invited_user_email_address} to service {invite_data['service_id']}"
)
debug_msg("accepted invite")
# We need to avoid taking a second trip through the login.gov code because we cannot pull the
# access token twice. So once we retrieve these values, let's park them in redis for 15 minutes
put_invite_data_in_redis(
state, invite_data, user_email, user_uuid, invited_user_email_address
)
form = SetupUserProfileForm()
if (
form.validate_on_submit()
and redis_client.raw_get(f"invitedata-{state}") is not None
):
invite_data, user_email, user_uuid, invited_user_email_address = (
get_invite_data_from_redis(state)
)
# create or update the user
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
if user is None:
user = User.register(
@@ -191,6 +206,9 @@ def _handle_login_dot_gov_invite(code, state, form):
auth_type="sms_auth",
)
debug_msg(f"registered user {form.name.data} with email {user_email}")
else:
user.update(mobile_number=form.mobile_number.data, name=form.name.data)
debug_msg(f"updated user {form.name.data}")
# activate the user
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
@@ -207,3 +225,32 @@ def _handle_login_dot_gov_invite(code, state, form):
f"Added user {usr.email_address} to service {invite_data['service_id']}"
)
return redirect(url_for("main.show_accounts_or_dashboard"))
elif login_gov_error:
current_app.logger.error(f"login.gov error: {login_gov_error}")
abort(403)
# we take two trips through this method, but should only hit this
# line on the first trip. On the second trip, we should get redirected
# to the accounts page because we have successfully registered.
return render_template("views/set-up-your-profile.html", form=form)
def get_invited_user_email_address(invited_user_id):
# InvitedUser is an unhashable type and hard to mock in tests
# so this convenience method is a workaround for that
invited_user = InvitedUser.by_id(invited_user_id)
return invited_user.email_address
def invited_user_accept_invite(invited_user_id):
invited_user = InvitedUser.by_id(invited_user_id)
if invited_user.status == "expired":
current_app.logger.error("User invitation has expired")
flash("Your invitation has expired.")
abort(401)
invited_user.accept_invite()
def debug_msg(msg):
current_app.logger.debug(hilite(msg))
+4 -4
View File
@@ -7,10 +7,6 @@ from flask import abort, flash, redirect, render_template, request, session, url
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
from notifications_utils.recipients import RecipientCSV, first_column_headings
from notifications_utils.sanitise_text import SanitiseASCII
from xlrd.biffh import XLRDError
from xlrd.xldate import XLDateError
@@ -39,6 +35,10 @@ from app.utils import PermanentRedirect, should_skip_template_page, unicode_trun
from app.utils.csv import Spreadsheet, get_errors_for_csv
from app.utils.templates import get_template
from app.utils.user import user_has_permissions
from notifications_utils import SMS_CHAR_COUNT_LIMIT
from notifications_utils.insensitive_dict import InsensitiveDict
from notifications_utils.recipients import RecipientCSV, first_column_headings
from notifications_utils.sanitise_text import SanitiseASCII
def get_example_csv_fields(column_headers, use_example_as_example, submitted_fields):
+1 -1
View File
@@ -6,7 +6,6 @@ import jwt
import requests
from flask import Response, current_app, redirect, render_template, request, url_for
from flask_login import current_user
from notifications_utils.url_safe_token import generate_token
from app import login_manager, user_api_client
from app.main import main
@@ -16,6 +15,7 @@ from app.models.user import User
from app.utils import hide_from_search_engines
from app.utils.login import is_safe_redirect_url
from app.utils.time import is_less_than_days_ago
from notifications_utils.url_safe_token import generate_token
def _reformat_keystring(orig):
+1 -1
View File
@@ -4,7 +4,6 @@ from flask import abort, flash, jsonify, redirect, render_template, request, url
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 app import (
current_service,
@@ -30,6 +29,7 @@ from app.models.template_list import TemplateList, TemplateLists
from app.utils import NOTIFICATION_TYPES, should_skip_template_page
from app.utils.templates import get_template
from app.utils.user import user_has_permissions
from notifications_utils import SMS_CHAR_COUNT_LIMIT
form_objects = {
"email": EmailTemplateForm,
+1 -1
View File
@@ -3,7 +3,6 @@ import json
from flask import current_app, redirect, render_template, request, session, url_for
from flask_login import current_user
from itsdangerous import SignatureExpired
from notifications_utils.url_safe_token import check_token
from app import user_api_client
from app.main import main
@@ -15,6 +14,7 @@ from app.utils.login import (
redirect_to_sign_in,
redirect_when_logged_in,
)
from notifications_utils.url_safe_token import check_token
@main.route("/two-factor-email-sent", methods=["GET"])
+1 -1
View File
@@ -11,7 +11,6 @@ from flask import (
url_for,
)
from flask_login import current_user
from notifications_utils.url_safe_token import check_token
from app import user_api_client
from app.event_handlers import (
@@ -31,6 +30,7 @@ from app.main.forms import (
)
from app.models.user import User
from app.utils.user import user_is_gov_user, user_is_logged_in
from notifications_utils.url_safe_token import check_token
NEW_EMAIL = "new-email"
NEW_MOBILE = "new-mob"
+1 -1
View File
@@ -2,13 +2,13 @@ import json
from flask import abort, current_app, flash, redirect, render_template, session, url_for
from itsdangerous import SignatureExpired
from notifications_utils.url_safe_token import check_token
from app import user_api_client
from app.main import main
from app.main.forms import TwoFactorForm
from app.models.user import User
from app.utils.login import redirect_to_sign_in
from notifications_utils.url_safe_token import check_token
@main.route("/verify", methods=["GET", "POST"])
+1
View File
@@ -1,6 +1,7 @@
from abc import abstractmethod
from flask import abort
from notifications_utils.serialised_model import (
SerialisedModel,
SerialisedModelCollection,
+1 -2
View File
@@ -1,10 +1,9 @@
from abc import ABC, abstractmethod
from notifications_utils.formatters import formatted_list
from app.formatters import format_thousands
from app.models import ModelList
from app.notify_client.service_api_client import service_api_client
from notifications_utils.formatters import formatted_list
class Event(ABC):
+1 -1
View File
@@ -1,5 +1,4 @@
from flask import abort, current_app
from notifications_utils.serialised_model import SerialisedModelCollection
from werkzeug.utils import cached_property
from app.models import JSONModel, SortByNameMixin
@@ -15,6 +14,7 @@ from app.notify_client.organizations_api_client import organizations_client
from app.notify_client.service_api_client import service_api_client
from app.notify_client.template_folder_api_client import template_folder_api_client
from app.utils import get_default_sms_sender
from notifications_utils.serialised_model import SerialisedModelCollection
class Service(JSONModel, SortByNameMixin):
+1 -1
View File
@@ -2,9 +2,9 @@ from flask import abort, has_request_context, request
from flask_login import current_user
from notifications_python_client import __version__
from notifications_python_client.base import BaseAPIClient
from notifications_utils.clients.redis import RequestCache
from app.extensions import redis_client
from notifications_utils.clients.redis import RequestCache
cache = RequestCache(redis_client)
+1 -1
View File
@@ -1,7 +1,6 @@
import uuid
from flask import current_app
from notifications_utils.s3 import s3upload as utils_s3upload
from app.s3_client import (
get_s3_contents,
@@ -9,6 +8,7 @@ from app.s3_client import (
get_s3_object,
set_s3_metadata,
)
from notifications_utils.s3 import s3upload as utils_s3upload
FILE_LOCATION_STRUCTURE = "service-{}-notify/{}.csv"
+1 -1
View File
@@ -2,9 +2,9 @@ import uuid
from boto3 import Session
from flask import current_app
from notifications_utils.s3 import s3upload as utils_s3upload
from app.s3_client import get_s3_object
from notifications_utils.s3 import s3upload as utils_s3upload
TEMP_TAG = "temp-{user_id}_"
EMAIL_LOGO_LOCATION_STRUCTURE = "{temp}{unique_id}-{filename}"
@@ -1,10 +1,17 @@
{% from "../components/banner.html" import banner %}
{% from "../components/components/skip-link/macro.njk" import usaSkipLink -%}
{% from "components/banner.html" import banner %}
{% from "components/components/skip-link/macro.njk" import usaSkipLink -%}
{% from "components/sub-navigation.html" import sub_navigation %}
<!DOCTYPE html>
<html lang="{{ htmlLang | default('en') }}" class="{{ htmlClasses }}">
{% include "new/components/head.html" %}
<head>
<title>
{% block pageTitle %}
{% block per_page_title %}{% endblock %} Notify.gov
{% endblock %}
</title>
{% include "new/components/head.html" %}
</head>
<body class="usa-template__body {{ bodyClasses }}">
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
{% block bodyStart %}
@@ -23,11 +30,10 @@
}) }}
{% endblock %}
{% block header %}
{% if current_user.is_authenticated %}
{% include 'new/components/usa_banner.html' %}
{% include 'new/components/header.html' %}
{% endif %}
{% include 'new/components/usa_banner.html' %}
{% include 'new/components/header.html' %}
{% endblock %}
{% block main %}
@@ -36,18 +42,17 @@
{% block backLink %}{% endblock %}
{% endblock %}
{% block mainClasses %}
<!-- notes set mainClasses = "margin-top-5 padding-bottom-5" where withoutnav_template was used and maybe templates that are using content_template -->
{% set mainClasses = "margin-top-5 padding-bottom-5" %}
<main class="{{ mainClasses }}" id="main-content" role="main">
{% endblock %}
{% block content %}
{% block flash_messages %}
<!-- flash_message.html was from the withoutnav_template and is only included on child templates that was using withoutnav_template. Now, we can add in flash_message blocks and include 'flash_messages.html' to child templates that was using withoutnav_template. This will help to eliminate the use of a whole other parent template. -->
{% include 'flash_messages.html' %}
{% endblock %}
{% block maincolumn_content %}
{% block fromContentTemplatetwoColumnGrid %}
<div class="grid-row">
{% if navigation_links %}
<div class="tablet:grid-col-2">
<div class="tablet:grid-col-2 margin-bottom-4">
{{ sub_navigation(navigation_links) }}
</div>
<div class="tablet:grid-col-10 padding-left-4 usa-prose site-prose">
@@ -57,8 +62,6 @@
{% block content_column_content %}{% endblock %}
</div>
</div>
<!-- content_column_content block is from the content_template.html. We do not need this template. We can consolidate and move it to the base.html template. We can call on this block where child templates were using the content_template.html -->
{% endblock %}
{% endblock %}
{% endblock %}
</main>
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}Youre not authorised to see this page{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}Youre not allowed to see this page{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}Page not found{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}Page not found{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}Sorry, theres a problem with the service{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
+3 -11
View File
@@ -1,6 +1,4 @@
<head>
<meta charset="utf-8" />
<title>{% block pageTitle %}Notify.gov{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Reach people where they are with a text message platform built just for government">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="f0f0f0" />
@@ -12,7 +10,7 @@
{# Ensure that older IE versions always render with the correct rendering engine #}
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
{% block headIcons %}
<link rel="shortcut icon" href="{{ assetPath | default('/assets') }}/images/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
@@ -22,25 +20,19 @@
<meta name="msapplication-TileColor" content="#da532c">
<link href="{{ assetPath | default('/assets') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff">
{% endblock %}
<link rel="stylesheet" media="screen" href="{{ asset_url('css/styles.css') }}" />
{% block extra_stylesheets %}{% endblock %}
{% block meta_format_detection %}
<meta name="format-detection" content="telephone=no">
{% endblock %}
{% block og_image %}
<meta property="og:title" content="Notify.gov">
<meta property="og:description" content="Reach people where they are with a text message platform built just for government">
<meta property="og:image" content="/static/images/notify-og-image.png">
{% endblock %}
{# google #}
<script type="text/javascript" src="{{ asset_url('js/gtm_head.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/chart.umd.js') }}"></script>
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
{% if g.hide_from_search_engines %}
<meta name="robots" content="noindex" />
{% endif %}
{% block head %}{% endblock %}
</head>
+22 -19
View File
@@ -1,25 +1,28 @@
{# setting navigation and secondarynavigation #}
{% 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')}
] %}
{% if current_user.platform_admin %}
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin')}] %}
{% else %}
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile')}] %}
{% 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"}
{% if current_user.is_authenticated %}
{% 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')}
] %}
{% else %}
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
{% if current_user.platform_admin %}
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin')}] %}
{% else %}
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile')}] %}
{% 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 %}
{# usa header #}
<header class="usa-header usa-header--extended">
<div class="usa-nav-container">
+7 -13
View File
@@ -6,10 +6,9 @@ This document serves as a glossary for the templates directory structure of the
## 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`, `settings_templates`, and `content_template`.
- `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 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
- `withnav_template.html`: A variation of org_template and settings_template that includes a sidebar.
- **/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)
- `head.html`: Template for the site's <head>, included in `base.html`.
- `header.html`: Template for the site's header, included in `base.html`.
@@ -23,20 +22,15 @@ This document serves as a glossary for the templates directory structure of the
### 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. This is also true for `usaHeader` macro component.
- Employ **components** `{% include %}` for reusable UI elements.
### Old Layout Templates We Don't Need
- withoutnav_template.html Delete
- main_template.html Delete
- withoutnav_template.html Delete, it is now apart of base.html
- main_template.html Delete, it is now apart of base.html
- settings_templates.html `withnav_template` can be used to replace `settings_template`.
- settings_nav.html (move to /components/ directory)
- main_nav.html (move to /components/ directory)
- service_navigation.html (move to /components/ directory)
- org_template, could be under it's own directory called /layout/organization
- org_template, Delete, it is now apart of withnav_template.html
- org_nav.html (move to /components/ directory)
- content_template.html Delete
- content_template.html Delete, it is now apart of base.html
@@ -291,10 +291,12 @@
{% block bodyEnd %}
{% block extra_javascripts %}
{% endblock %}
<!--[if gt IE 8]><!-->
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
<!--<![endif]-->
{% endblock %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/sub-navigation.html" import sub_navigation %}
{% from "components/page-header.html" import page_header %}
@@ -1,4 +1,4 @@
{% extends "admin_template.html" %}
{% extends "base.html" %}
{% set mainClasses = "margin-top-5 padding-bottom-5" %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "admin_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
{% block org_page_title %}{% endblock %} {{ current_org.name }}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "admin_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
{% block service_page_title %}{% endblock %} {{ current_service.name }}
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/service-link.html" import service_link %}
{% from "components/content-metadata.html" import content_metadata %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}The invitation you were sent has been cancelled{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Check your email
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/components/button/macro.njk" import usaButton %}
{% macro service_list(
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Documentation
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/components/button/macro.njk" import usaButton %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% from "components/sub-navigation.html" import sub_navigation %}
{% from "components/service-link.html" import service_link %}
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% from "components/service-link.html" import service_link %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% from "components/sub-navigation.html" import sub_navigation %}
{% from "components/components/details/macro.njk" import usaDetails %}
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/service-link.html" import service_link %}
{% block per_page_title %}
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/service-link.html" import service_link %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% from "components/sub-navigation.html" import sub_navigation %}
{% from "components/service-link.html" import service_link %}
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Send files by email
+1 -1
View File
@@ -1,5 +1,5 @@
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Integration testing
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/table.html" import mapping_table, row, text_field %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Check your email
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/big-number.html" import big_number %}
{% from "components/page-header.html" import page_header %}
{% from "components/table.html" import field, list_table %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% block main %}
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% block per_page_title %}
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/copy-to-clipboard.html" import copy_to_clipboard %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% block per_page_title %}
+1 -1
View File
@@ -3,7 +3,7 @@
{% from "components/live-search.html" import live_search %}
{% from "components/components/details/macro.njk" import usaDetails %}
{% extends "content_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Message parts
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/content-metadata.html" import content_metadata %}
{% block per_page_title %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% block per_page_title %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Check your email
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% from "components/content-metadata.html" import content_metadata %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/select/macro.njk" import usaSelect -%}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "admin_template.html" %}
{% extends "base.html" %}
{% from "components/components/button/macro.njk" import usaButton %}
{% block meta %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% from "components/banner.html" import banner_wrapper %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "content_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
Trial mode
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/components/button/macro.njk" import usaButton %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/form.html" import form_wrapper %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/components/button/macro.njk" import usaButton %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/sub-navigation.html" import sub_navigation %}
{% block per_page_title %}
@@ -1,4 +1,4 @@
{% extends "withoutnav_template.html" %}
{% extends "base.html" %}
{% from "components/components/button/macro.njk" import usaButton %}
{% block per_page_title %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "admin_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
{% block service_page_title %}{% endblock %} {{ current_service.name }}
+2 -1
View File
@@ -3,11 +3,12 @@ from itertools import chain
from flask import abort, g, make_response, request
from flask_login import current_user
from notifications_utils.field import Field
from ordered_set import OrderedSet
from werkzeug.datastructures import MultiDict
from werkzeug.routing import RequestRedirect
from notifications_utils.field import Field
SENDING_STATUSES = ["created", "pending", "sending"]
DELIVERED_STATUSES = ["delivered", "sent"]
FAILURE_STATUSES = [

Some files were not shown because too many files have changed in this diff Show More