mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-21 23:06:34 -04:00
Compare commits
51 Commits
05-10-2024
...
create-sta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d94e45da3 | ||
|
|
3a6e5e9c2d | ||
|
|
ed827d0c29 | ||
|
|
8e0724effa | ||
|
|
3caf1e1631 | ||
|
|
6ce5856f3b | ||
|
|
f91756d2c3 | ||
|
|
1b6741d0e5 | ||
|
|
fe9e25ba6f | ||
|
|
aa175aa4be | ||
|
|
6f59cff0c4 | ||
|
|
33d402f9b9 | ||
|
|
90d3dde6b5 | ||
|
|
68168a85a1 | ||
|
|
3a42aafad2 | ||
|
|
ec6bdee427 | ||
|
|
1b172553f5 | ||
|
|
a0e17816e6 | ||
|
|
7302e4c811 | ||
|
|
dcfc78d805 | ||
|
|
e660541e9c | ||
|
|
9a83ba7475 | ||
|
|
7ff0cec147 | ||
|
|
d4a46ae552 | ||
|
|
6855a6ebe4 | ||
|
|
5546de66c4 | ||
|
|
6bab682edd | ||
|
|
e91723fc26 | ||
|
|
b3a401a4c7 | ||
|
|
d484c3636b | ||
|
|
013154d5cf | ||
|
|
b5c7a71215 | ||
|
|
8a6802be8f | ||
|
|
e973c11d3b | ||
|
|
c578175e53 | ||
|
|
11beff2541 | ||
|
|
7f72599401 | ||
|
|
5506da56e2 | ||
|
|
cdd71fa07d | ||
|
|
065f1b7c15 | ||
|
|
7108c5f1c8 | ||
|
|
65899f6171 | ||
|
|
bd633ddc98 | ||
|
|
bc945c9420 | ||
|
|
ff1956ce70 | ||
|
|
2c0a4f16f2 | ||
|
|
d47fbb8eb7 | ||
|
|
98f12980ad | ||
|
|
2d3dbbc532 | ||
|
|
fcf2e31911 | ||
|
|
2454a6e94b |
9
Makefile
9
Makefile
@@ -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
README.md
18
README.md
@@ -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
|
||||
|
||||
@@ -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
app/assets/javascripts/chartDashboard.js
Normal file
24
app/assets/javascripts/chartDashboard.js
Normal 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);
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
@@ -653,17 +653,18 @@ def preview_job(service_id, template_id, upload_id, row_index=2):
|
||||
data = _check_messages(
|
||||
service_id, template_id, upload_id, row_index, force_hide_sender=True
|
||||
)
|
||||
create_job(service_id, upload_id)
|
||||
|
||||
return render_template(
|
||||
"views/check/preview.html",
|
||||
scheduled_for=session["scheduled_for"],
|
||||
scheduled_for=session.get("scheduled_for"),
|
||||
**data,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
|
||||
@main.route("/services/<uuid:service_id>/create-job/<uuid:upload_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def start_job(service_id, upload_id):
|
||||
def create_job(service_id, upload_id):
|
||||
scheduled_for = session.pop("scheduled_for", None)
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
@@ -671,8 +672,12 @@ def start_job(service_id, upload_id):
|
||||
scheduled_for=scheduled_for,
|
||||
)
|
||||
|
||||
session.pop("sender_id", None)
|
||||
|
||||
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def start_job(service_id, upload_id):
|
||||
job_api_client.start_job(service_id, upload_id)
|
||||
session.pop("sender_id", None)
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.view_job",
|
||||
@@ -908,36 +913,7 @@ def preview_notification(service_id, template_id):
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
|
||||
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
||||
|
||||
return render_template(
|
||||
"views/notifications/preview.html",
|
||||
**_check_notification(
|
||||
service_id, template_id, show_recipient=False, force_hide_sender=True
|
||||
),
|
||||
scheduled_for=session["scheduled_for"],
|
||||
recipient=recipient,
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_notification(service_id, template_id):
|
||||
scheduled_for = session.pop("scheduled_for", "")
|
||||
recipient = get_recipient()
|
||||
if not recipient:
|
||||
return redirect(
|
||||
url_for(
|
||||
".send_one_off",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
for k, v in session["placeholders"].items():
|
||||
@@ -968,10 +944,39 @@ def send_notification(service_id, template_id):
|
||||
notification_count=1,
|
||||
valid="True",
|
||||
)
|
||||
session["recipient"] = recipient
|
||||
session["upload_id"] = upload_id
|
||||
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
||||
|
||||
session.pop("recipient")
|
||||
session.pop("placeholders")
|
||||
return render_template(
|
||||
"views/notifications/preview.html",
|
||||
**_check_notification(
|
||||
service_id, template_id, show_recipient=False, force_hide_sender=True
|
||||
),
|
||||
upload_id=upload_id,
|
||||
scheduled_for=session["scheduled_for"],
|
||||
recipient=recipient,
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
if not recipient:
|
||||
return redirect(
|
||||
url_for(
|
||||
".send_one_off",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
|
||||
upload_id = session.pop("upload_id")
|
||||
job_api_client.start_job(service_id, upload_id)
|
||||
# We have to wait for the job to run and create the notification in the database
|
||||
time.sleep(0.1)
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
@@ -1000,12 +1005,15 @@ def send_notification(service_id, template_id):
|
||||
job_id=upload_id,
|
||||
)
|
||||
)
|
||||
|
||||
session.pop("recipient")
|
||||
session.pop("placeholders")
|
||||
return redirect(
|
||||
url_for(
|
||||
".view_job",
|
||||
service_id=service_id,
|
||||
job_id=upload_id,
|
||||
from_job=upload_id,
|
||||
notification_id=notifications["notifications"][0]["id"],
|
||||
# used to show the final step of the tour (help=3) or not show
|
||||
# a back link on a just sent one off notification (help=0)
|
||||
help=request.args.get("help"),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from flask import abort
|
||||
|
||||
from notifications_utils.serialised_model import (
|
||||
SerialisedModel,
|
||||
SerialisedModelCollection,
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -147,5 +147,10 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
url="/service/{}/job/{}/cancel".format(service_id, job_id), data={}
|
||||
)
|
||||
|
||||
def start_job(self, service_id, job_id):
|
||||
return self.post(
|
||||
url="/service/{}/job/{}/start-job".format(service_id, job_id), data={}
|
||||
)
|
||||
|
||||
|
||||
job_api_client = JobApiClient()
|
||||
|
||||
@@ -37,11 +37,10 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
||||
# we do not want in our logs, so we do a POST request instead of a GET
|
||||
method = self.post if to else self.get
|
||||
kwargs = {"data": params} if to else {"params": params}
|
||||
|
||||
if job_id:
|
||||
return method(
|
||||
url="/service/{}/job/{}/notifications".format(service_id, job_id),
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
if limit_days is not None:
|
||||
@@ -100,5 +99,8 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
||||
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
|
||||
)["count"]
|
||||
|
||||
def get_total_notification_message_parts_by_job_id(self, job_id):
|
||||
return self.get(url="/notifications/{}/message_parts".format(job_id))
|
||||
|
||||
|
||||
notification_api_client = NotificationApiClient()
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% block per_page_title %}You’re not authorised to see this page{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
<div class="grid-row">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% block per_page_title %}You’re not allowed to see this page{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
<div class="grid-row">
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% block per_page_title %}Sorry, there’s a problem with the service{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
<div class="grid-row">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
{% extends "admin_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{% block org_page_title %}{% endblock %} – {{ current_org.name }}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
<p>
|
||||
The following content on the <a class="usa-link" href="https://status.notifications.service.gov.uk/">Notify status page</a> is not compliant with the WCAG version 2.1 AA standard:
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<ul class="usa-list">
|
||||
<li>an incorrect heading hierarchy which fails <a class="usa-link" href="https://www.w3.org/TR/WCAG21/#info-and-relationships">success criterion 1.3.1: info and relationships</a></li>
|
||||
|
||||
@@ -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" %}
|
||||
{% 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,4 +1,4 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
|
||||
{% macro service_list(
|
||||
|
||||
@@ -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,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/components/button/macro.njk" import usaButton %}
|
||||
|
||||
{% block per_page_title %}
|
||||
|
||||
@@ -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,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 "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,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,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,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,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 %}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<!-- <p>Placeholder: This message will be delivered to <b>400 phone numbers</b> and will use a total of <b>800 message parts</b>, leaving Washington DSHS with <b>249,200 message parts remaining</b>.</p> -->
|
||||
|
||||
<h3>Does everything look good?</h3>
|
||||
{% if not error %}
|
||||
{% set button_text %}
|
||||
|
||||
@@ -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 %}
|
||||
Check your email
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
{% extends "content_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
|
||||
{% block per_page_title %}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% from "components/content-metadata.html" import content_metadata %}
|
||||
|
||||
{% block per_page_title %}
|
||||
@@ -61,7 +61,7 @@
|
||||
processed and when it’s stored.</p>
|
||||
|
||||
<!-- <p>Your personal data will be processed both in the UK and the European Economic Area (EEA). Your data receives the same level of protection in the EEA as it does in the UK through the safeguard of <a class="usa-link" href="https://ico.org.uk/for-organizations/dp-at-the-end-of-the-transition-period/data-protection-and-the-eu-in-detail/adequacy/">Adequacy Decisions</a>.</p> -->
|
||||
|
||||
|
||||
<h2 class="font-body-lg" id="how-we-protect-your-data">How we protect your data and keep it secure</h2>
|
||||
|
||||
<p>We are committed to doing all that we can to keep your data secure. To prevent unauthorized access or disclosure we have put in place technical and organizational procedures to secure the data we collect about you – for example, we protect your
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<!-- <h2 class="font-body-lg" id="childrens-privacy-protection">Children’s privacy protection</h2>
|
||||
|
||||
<p>We understand the importance of protecting children’s privacy online. Our services are not designed for, or intentionally targeted at, children 13 years of age or younger. It is not our policy to intentionally collect or maintain data about anyone under the age of 13.</p>
|
||||
<p>We understand the importance of protecting children’s privacy online. Our services are not designed for, or intentionally targeted at, children 13 years of age or younger. It is not our policy to intentionally collect or maintain data about anyone under the age of 13.</p>
|
||||
|
||||
<h2 class="font-body-lg" id="your-rights">What are your rights</h2>
|
||||
|
||||
|
||||
@@ -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,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,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,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,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,4 +1,4 @@
|
||||
{% extends "admin_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
|
||||
{% block meta %}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
{% extends "content_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
|
||||
{% 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 "content_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Trial mode
|
||||
|
||||
@@ -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-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,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,4 +1,4 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% from "components/sub-navigation.html" import sub_navigation %}
|
||||
|
||||
{% block per_page_title %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user