mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-04 05:31:44 -04:00
merge main
This commit is contained in:
@@ -52,8 +52,7 @@ from app.formatters import (
|
||||
format_datetime_human,
|
||||
format_datetime_normal,
|
||||
format_datetime_relative,
|
||||
format_datetime_short,
|
||||
format_datetime_short_america,
|
||||
format_datetime_table,
|
||||
format_day_of_week,
|
||||
format_delta,
|
||||
format_delta_days,
|
||||
@@ -552,8 +551,7 @@ def add_template_filters(application):
|
||||
format_datetime,
|
||||
format_datetime_24h,
|
||||
format_datetime_normal,
|
||||
format_datetime_short,
|
||||
format_datetime_short_america,
|
||||
format_datetime_table,
|
||||
valid_phone_number,
|
||||
linkable_name,
|
||||
format_date,
|
||||
|
||||
@@ -62,6 +62,14 @@
|
||||
margin: -20px units(1) 20px units(1);
|
||||
}
|
||||
|
||||
h2.sms-message-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h2.recipient-list {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sms-message-status-outbound {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -319,27 +319,15 @@ td.table-empty-message {
|
||||
|
||||
.dashboard {
|
||||
.big-number-with-status {
|
||||
a {
|
||||
background: color("blue-60v");
|
||||
padding: units(2);
|
||||
margin-bottom: units(1);
|
||||
text-decoration: none;
|
||||
&:hover{
|
||||
background: color("blue-warm-70v");
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
.big-number-smaller {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.big-number-number {
|
||||
font-size: units(5);
|
||||
line-height: units(6);
|
||||
line-height: units(5);
|
||||
}
|
||||
.big-number-label {
|
||||
font-size: units(3);
|
||||
font-size: units(2.5);
|
||||
}
|
||||
}
|
||||
.big-number-status {
|
||||
|
||||
@@ -9,5 +9,9 @@ in the form $setting: value,
|
||||
|
||||
@use "uswds-core" with (
|
||||
$theme-font-type-sans: "public-sans",
|
||||
$theme-show-notifications: false
|
||||
$theme-show-notifications: false,
|
||||
$theme-banner-max-width: "desktop-lg",
|
||||
$theme-grid-container-max-width: "desktop-lg",
|
||||
$theme-footer-max-width: "desktop-lg",
|
||||
$theme-header-max-width: "desktop-lg"
|
||||
);
|
||||
|
||||
@@ -38,11 +38,6 @@ class Config(object):
|
||||
NR_MONITOR_ON = settings and settings.monitor_mode
|
||||
COMMIT_HASH = getenv("COMMIT_HASH", "--------")[0:7]
|
||||
|
||||
TEMPLATE_PREVIEW_API_HOST = getenv(
|
||||
"TEMPLATE_PREVIEW_API_HOST", "http://localhost:9999"
|
||||
)
|
||||
TEMPLATE_PREVIEW_API_KEY = getenv("TEMPLATE_PREVIEW_API_KEY", "my-secret-key")
|
||||
|
||||
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov"]
|
||||
|
||||
# Logging
|
||||
|
||||
@@ -22,7 +22,7 @@ from notifications_utils.recipients import InvalidPhoneError, validate_phone_num
|
||||
from notifications_utils.take import Take
|
||||
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
from app.utils.time import parse_naive_dt
|
||||
from app.utils.time import parse_dt, parse_naive_dt
|
||||
|
||||
|
||||
def apply_html_class(tags, html_file):
|
||||
@@ -87,37 +87,22 @@ def format_time(date):
|
||||
|
||||
|
||||
def format_datetime_normal(date):
|
||||
# example: February 20, 2024 at 07:00 PM US/Eastern, used for datetimes that's not within tables
|
||||
return "{} at {} {}".format(
|
||||
format_date_normal(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
format_date_normal(date), format_time_12h(date), get_user_preferred_timezone()
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_short(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date_short(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_short_america(date):
|
||||
return "{} at {}".format(format_date_numeric_america(date), format_time_12h(date))
|
||||
|
||||
|
||||
def format_date_numeric_america(date):
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
|
||||
)
|
||||
def format_datetime_table(date):
|
||||
# example: 03-18-2024 at 04:53 PM, intended for datetimes in tables
|
||||
return "{} at {}".format(format_date_numeric(date), format_time_12h(date))
|
||||
|
||||
|
||||
def format_time_12h(date):
|
||||
date = parse_naive_dt(date)
|
||||
date = parse_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
|
||||
)
|
||||
return date.astimezone(preferred_tz).strftime("%I:%M %p")
|
||||
|
||||
|
||||
def format_datetime_relative(date):
|
||||
@@ -137,7 +122,7 @@ def format_date_numeric(date):
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%Y-%m-%d")
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
|
||||
)
|
||||
|
||||
|
||||
@@ -186,7 +171,7 @@ def format_date(date):
|
||||
|
||||
def format_date_normal(date):
|
||||
date = parse_naive_dt(date)
|
||||
return date.strftime("%d %B %Y").lstrip("0")
|
||||
return date.strftime("%B %d, %Y").lstrip("0")
|
||||
|
||||
|
||||
def format_date_short(date):
|
||||
|
||||
@@ -605,6 +605,15 @@ class RegisterUserForm(StripWhitespaceForm):
|
||||
auth_type = HiddenField("auth_type", default="sms_auth")
|
||||
|
||||
|
||||
class SetupUserProfileForm(StripWhitespaceForm):
|
||||
name = GovukTextInputField(
|
||||
"Full name", validators=[DataRequired(message="Cannot be empty")]
|
||||
)
|
||||
mobile_number = international_phone_number()
|
||||
# TODO This should be replaced with a select widget when one is available.
|
||||
preferred_timezone = HiddenField("preferred_timezone", default="US/Eastern")
|
||||
|
||||
|
||||
class RegisterUserFromInviteForm(RegisterUserForm):
|
||||
def __init__(self, invited_user):
|
||||
super().__init__(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from flask import abort, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
@@ -8,6 +10,8 @@ from app.main.views.pricing import CURRENT_SMS_RATE
|
||||
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
|
||||
from app.utils.user import user_is_logged_in
|
||||
|
||||
login_dot_gov_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
|
||||
|
||||
@main.route("/")
|
||||
def index():
|
||||
@@ -18,6 +22,7 @@ def index():
|
||||
"views/signedout.html",
|
||||
sms_rate=CURRENT_SMS_RATE,
|
||||
counts=status_api_client.get_count_of_live_services_and_organizations(),
|
||||
login_dot_gov_url=login_dot_gov_url,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from flask import (
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
stream_with_context,
|
||||
url_for,
|
||||
)
|
||||
@@ -18,7 +19,7 @@ from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTem
|
||||
|
||||
from app import (
|
||||
current_service,
|
||||
format_datetime_short,
|
||||
format_datetime_table,
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
)
|
||||
@@ -93,7 +94,7 @@ def view_job_csv(service_id, job_id):
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": 'inline; filename="{} - {}.csv"'.format(
|
||||
job.template["name"], format_datetime_short(job.created_at)
|
||||
job.template["name"], format_datetime_table(job.created_at)
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -373,6 +374,13 @@ def get_job_partials(job):
|
||||
job.template_type
|
||||
)
|
||||
|
||||
if request.referrer is not None:
|
||||
session["arrived_from_preview_page"] = "check" in request.referrer
|
||||
else:
|
||||
session["arrived_from_preview_page"] = False
|
||||
|
||||
arrived_from_preview_page_url = session.get("arrived_from_preview_page", False)
|
||||
|
||||
return {
|
||||
"counts": counts,
|
||||
"notifications": render_template(
|
||||
@@ -396,6 +404,7 @@ def get_job_partials(job):
|
||||
"status": render_template(
|
||||
"partials/jobs/status.html",
|
||||
job=job,
|
||||
arrived_from_preview_page_url=arrived_from_preview_page_url,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import abort, redirect, render_template, session, url_for
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
|
||||
from app import user_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
RegisterUserForm,
|
||||
RegisterUserFromInviteForm,
|
||||
RegisterUserFromOrgInviteForm,
|
||||
SetupUserProfileForm,
|
||||
)
|
||||
from app.main.views import sign_in
|
||||
from app.main.views.verify import activate_user
|
||||
from app.models.user import InvitedOrgUser, InvitedUser, User
|
||||
from app.utils import hide_from_search_engines
|
||||
@@ -120,4 +132,47 @@ def _do_registration(form, send_sms=True, send_email=True, organization_id=None)
|
||||
def registration_continue():
|
||||
if not session.get("user_details"):
|
||||
return redirect(url_for(".show_accounts_or_dashboard"))
|
||||
return render_template("views/registration-continue.html")
|
||||
else:
|
||||
raise Exception("Unexpected routing in registration_continue")
|
||||
|
||||
|
||||
@main.route("/set-up-your-profile", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def set_up_your_profile():
|
||||
|
||||
form = SetupUserProfileForm()
|
||||
|
||||
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:
|
||||
access_token = sign_in._get_access_token(code, state)
|
||||
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
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
|
||||
|
||||
# create the user
|
||||
# TODO we have to provide something for password until that column goes away
|
||||
# TODO ideally we would set the user's preferred timezone here as well
|
||||
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
if user is None:
|
||||
user = User.register(
|
||||
name=form.name.data,
|
||||
email_address=user_email,
|
||||
mobile_number=form.mobile_number.data,
|
||||
password=str(uuid.uuid4()),
|
||||
auth_type="sms_auth",
|
||||
)
|
||||
|
||||
# activate the user
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
activate_user(user["id"])
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
return render_template("views/set-up-your-profile.html", form=form)
|
||||
|
||||
@@ -1012,8 +1012,9 @@ def send_notification(service_id, template_id):
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".view_notification",
|
||||
".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
|
||||
|
||||
@@ -16,10 +16,12 @@ from flask import (
|
||||
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
|
||||
from app.main.forms import LoginForm
|
||||
from app.main.views.index import error
|
||||
from app.main.views.verify import activate_user
|
||||
from app.models.user import InvitedUser, User
|
||||
from app.utils import hide_from_search_engines
|
||||
@@ -77,9 +79,7 @@ def _get_user_email_and_uuid(access_token):
|
||||
return user_email, user_uuid
|
||||
|
||||
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in():
|
||||
def _do_login_dot_gov():
|
||||
# start login.gov
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
@@ -90,8 +90,13 @@ def sign_in():
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
# activate the user
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
activate_user(user["id"])
|
||||
try:
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
activate_user(user["id"])
|
||||
except BaseException as be: # noqa B036
|
||||
current_app.logger.error(be)
|
||||
error(401)
|
||||
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
|
||||
elif login_gov_error:
|
||||
@@ -99,6 +104,11 @@ def sign_in():
|
||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||
# end login.gov
|
||||
|
||||
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in():
|
||||
_do_login_dot_gov()
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
@@ -165,31 +175,17 @@ def sign_in():
|
||||
)
|
||||
|
||||
other_device = current_user.logged_in_elsewhere()
|
||||
notify_env = os.getenv("NOTIFY_ENVIRONMENT")
|
||||
current_app.logger.info("should render the sign in template")
|
||||
|
||||
# TODO REMOVE THIS INFO ONCE STAGING WORKS WITH LOGIN DOT GOV
|
||||
current_app.logger.info(f"NOTIFY ENV = {notify_env}")
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_CLIENT_ID={os.getenv('LOGIN_DOT_GOV_CLIENT_ID')}"
|
||||
token = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_USER_INFO_URL={os.getenv('LOGIN_DOT_GOV_USER_INFO_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_ACCESS_TOKEN_URL={os.getenv('LOGIN_DOT_GOV_ACCESS_TOKEN_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_LOGOUT_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_BASE_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_BASE_LOGOUT_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_SIGNOUT_REDIRECT={os.getenv('LOGIN_DOT_GOV_SIGNOUT_REDIRECT')}"
|
||||
)
|
||||
initial_signin_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
current_app.logger.info(f"LOGIN_DOT_GOV_INITIAL_SIGNIN_URL={initial_signin_url}")
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
# handle unit tests
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", token)
|
||||
url = url.replace("STATE", token)
|
||||
|
||||
return render_template(
|
||||
"views/signin.html",
|
||||
@@ -198,7 +194,7 @@ def sign_in():
|
||||
other_device=other_device,
|
||||
login_gov_enabled=True,
|
||||
password_reset_url=password_reset_url,
|
||||
initial_signin_url=initial_signin_url,
|
||||
initial_signin_url=url,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ from flask_login import current_user
|
||||
|
||||
from app.main import main
|
||||
|
||||
# ask login.gov if we really need manual logout and what's up with one hour sessions
|
||||
# ask login.gov how they recommend approaching dev environment
|
||||
# ask Tim Donaworth the same for #2
|
||||
|
||||
|
||||
def _sign_out_at_login_dot_gov():
|
||||
base_url = os.getenv("LOGIN_DOT_GOV_BASE_LOGOUT_URL")
|
||||
|
||||
@@ -5,6 +5,7 @@ from itsdangerous import SignatureExpired
|
||||
from notifications_utils.url_safe_token import check_token
|
||||
|
||||
from app import user_api_client
|
||||
from app.extensions import redis_client
|
||||
from app.main import main
|
||||
from app.main.forms import TwoFactorForm
|
||||
from app.models.user import InvitedOrgUser, InvitedUser, User
|
||||
@@ -64,20 +65,46 @@ def verify_email(token):
|
||||
|
||||
def activate_user(user_id):
|
||||
user = User.from_id(user_id)
|
||||
# the user will have a new current_session_id set by the API - store it in the cookie for future requests
|
||||
|
||||
# This is the login.gov path
|
||||
login_gov_invite_data = redis_client.get(f"service-invite-{user.email_address}")
|
||||
if login_gov_invite_data:
|
||||
login_gov_invite_data = json.loads(login_gov_invite_data.decode("utf8"))
|
||||
|
||||
# This is the deprecated path for organization invites where we get id from session
|
||||
session["current_session_id"] = user.current_session_id
|
||||
organization_id = session.get("organization_id")
|
||||
|
||||
activated_user = user.activate()
|
||||
activated_user.login()
|
||||
|
||||
# TODO when login.gov is mandatory, get rid of the if clause, it is deprecated.
|
||||
invited_user = InvitedUser.from_session()
|
||||
if invited_user:
|
||||
service_id = _add_invited_user_to_service(invited_user)
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
elif login_gov_invite_data:
|
||||
service_id = login_gov_invite_data["service_id"]
|
||||
|
||||
user.add_to_service(
|
||||
service_id,
|
||||
login_gov_invite_data["permissions"],
|
||||
login_gov_invite_data["folder_permissions"],
|
||||
login_gov_invite_data["from_user_id"],
|
||||
)
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
|
||||
# TODO when login.gov is mandatory, git rid of the if clause, it is deprecated.
|
||||
invited_org_user = InvitedOrgUser.from_session()
|
||||
if invited_org_user:
|
||||
user_api_client.add_user_to_organization(invited_org_user.organization, user_id)
|
||||
elif redis_client.get(f"organization-invite-{user.email_address}"):
|
||||
organization_id = redis_client.raw_get(
|
||||
f"organization-invite-{user.email_address}"
|
||||
)
|
||||
user_api_client.add_user_to_organization(
|
||||
organization_id.decode("utf8"), user_id
|
||||
)
|
||||
|
||||
if organization_id:
|
||||
return redirect(url_for("main.organization_dashboard", org_id=organization_id))
|
||||
|
||||
@@ -50,8 +50,8 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
"/user/get-login-gov-user",
|
||||
data={"login_uuid": user_uuid, "email": email_address},
|
||||
)
|
||||
if user_data is None:
|
||||
raise Exception("User not found")
|
||||
if user_data is None or user_data.get("data") is None:
|
||||
return None
|
||||
return user_data["data"]
|
||||
|
||||
def get_user_by_email_or_none(self, email_address):
|
||||
|
||||
13
app/templates/components/components/select/README.md
Normal file
13
app/templates/components/components/select/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Select
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the select component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/select/).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "ID for each select box."
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Name property for the select."
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
"type": "array",
|
||||
"required": true,
|
||||
"description": "The items within the select component."
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Value for the option which should be selected. Use this as an alternative to setting the selected option on each individual item."
|
||||
},
|
||||
{
|
||||
"name": "disabled",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "If true, select box will be disabled. Use the disabled option on each individual item to only disable certain options."
|
||||
},
|
||||
{
|
||||
"name": "describedBy",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users."
|
||||
},
|
||||
{
|
||||
"name": "label",
|
||||
"type": "object",
|
||||
"required": true,
|
||||
"description": "The label used by the select component.",
|
||||
"isComponent": true
|
||||
},
|
||||
{
|
||||
"name": "hint",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Can be used to add a hint to the select component.",
|
||||
"isComponent": true
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the select."
|
||||
}
|
||||
]
|
||||
3
app/templates/components/components/select/macro.njk
Normal file
3
app/templates/components/components/select/macro.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro usaSelect(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
15
app/templates/components/components/select/template.njk
Normal file
15
app/templates/components/components/select/template.njk
Normal file
@@ -0,0 +1,15 @@
|
||||
{% set describedBy = params.describedBy if params.describedBy else "" %}
|
||||
<div class="usa-form-group">
|
||||
<label class="usa-label" for="{{ params.name }}">{{params.label}}</label>
|
||||
{% if params.hint %}
|
||||
<div class="usa-hint" id="{{ params.describedBy }}">{{ params.hint }}</div>
|
||||
{% endif %}
|
||||
<select class="usa-select {%- if params.classes %} {{ params.classes }}{% endif %}" id="{{ params.id }}" name="{{ params.name }}"
|
||||
{%- if params.value %} value="{{ params.value}}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
|
||||
{% for item in params.items %}
|
||||
<option value="{{ item.value }}" {%- if item.disabled == true %} selected disabled {% endif %}>{{ item.text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -171,8 +171,8 @@
|
||||
{% endif %}
|
||||
<p class="status-hint margin-0 width-card ">
|
||||
{{ notification.status|format_notification_status_as_time(
|
||||
notification.created_at|format_datetime_short_america,
|
||||
(notification.sent_at or notification.created_at)|format_datetime_short_america
|
||||
notification.created_at|format_datetime_table,
|
||||
(notification.sent_at or notification.created_at)|format_datetime_table
|
||||
) }}
|
||||
</p>
|
||||
{% if displayed_on_single_line %}</span>{% endif %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-8">
|
||||
<h1>You’re not authorized to see this page</h1>
|
||||
<p class="usa-body"><a class="usa-link" href="{{ url_for('main.sign_in' )}}">Sign in</a> to Notify.gov and try again.</p>
|
||||
<p class="usa-body">If you have been invited to join Notify.gov, <a class="usa-link" href="{{ url_for('main.sign_in' )}}">sign in</a> to Notify.gov using your Login.gov account and try again.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
e
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
|
||||
{% endif %}
|
||||
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% from "../components/banner.html" import banner %}
|
||||
{% from "../components/components/skip-link/macro.njk" import usaSkipLink -%}
|
||||
{% from "../components/components/header/macro.njk" import usaHeader -%}
|
||||
{% from "../components/components/footer/macro.njk" import usaFooter -%}
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -24,142 +24,13 @@
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
<!-- \\#region header -->
|
||||
{# region header #}
|
||||
{% block header %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.platform_admin %}
|
||||
{% 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.platform_admin_splash_page'),
|
||||
"text": "Platform admin",
|
||||
"active": header_navigation.is_selected('platform-admin')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.support'),
|
||||
"text": "Contact us",
|
||||
"active": header_navigation.is_selected('support')
|
||||
}
|
||||
] %}
|
||||
{% if current_service %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.service_settings', service_id=current_service.id),
|
||||
"text": "Settings",
|
||||
"active": secondary_navigation.is_selected('settings')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set navigation = [
|
||||
{
|
||||
"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')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.user_profile'),
|
||||
"text": "User profile",
|
||||
"active": header_navigation.is_selected('user-profile')
|
||||
}
|
||||
] %}
|
||||
{% if current_service %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.service_settings', service_id=current_service.id),
|
||||
"text": "Settings",
|
||||
"active": secondary_navigation.is_selected('settings')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- Add navigation back after pilot -->
|
||||
{# {% set navigation = [
|
||||
{
|
||||
"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')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_in'),
|
||||
"text": "Sign in",
|
||||
"active": header_navigation.is_selected('sign-in')
|
||||
}
|
||||
] %} #}
|
||||
{% include 'new/components/usa_banner.html' %}
|
||||
{% include 'new/components/header.html' %}
|
||||
{% endif %}
|
||||
|
||||
{{ usaHeader({
|
||||
"homepageUrl": url_for('main.show_accounts_or_dashboard'),
|
||||
"productName": "Notify",
|
||||
"navigation": navigation,
|
||||
"navigationClasses": "govuk-header__navigation--end",
|
||||
"secondaryNavigation": secondaryNavigation,
|
||||
"assetsPath": asset_path + "images"
|
||||
}) }}
|
||||
{% endblock %}
|
||||
<!-- \\#endregion -->
|
||||
|
||||
<!-- \\#region block main -->
|
||||
{% block main %}
|
||||
|
||||
76
app/templates/new/components/header.html
Normal file
76
app/templates/new/components/header.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{# 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"}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
|
||||
{% endif %}
|
||||
{# usa header #}
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.png' }}" alt="Notify.gov logo" class="usa-flag-logo margin-right-1">
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{% if navigation %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% if secondaryNavigation %}
|
||||
{% for item in secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
44
app/templates/new/components/usa_banner.html
Normal file
44
app/templates/new/components/usa_banner.html
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
{# usa banner #}
|
||||
<section class="usa-banner site-banner" aria-label="Official website of the United States government,,,,,,">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
<div class="usa-banner__inner">
|
||||
<div class="grid-col-auto">
|
||||
<img aria-hidden="true" class="usa-banner__header-flag" src="/static/img/us_flag_small.png" alt="" />
|
||||
</div>
|
||||
<div class="grid-col-fill tablet:grid-col-auto" aria-hidden="true">
|
||||
<p class="usa-banner__header-text">An official website of the United States government</p>
|
||||
<p class="usa-banner__header-action">Here’s how you know</p>
|
||||
</div>
|
||||
<button type="button" class="usa-accordion__button usa-banner__button" aria-expanded="false" aria-controls="gov-banner">
|
||||
<span class="usa-banner__button-text">Here’s how you know</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="usa-banner__content usa-accordion__content" id="gov-banner" hidden="">
|
||||
<div class="grid-row grid-gap-lg">
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-dot-gov.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Official websites use .gov</strong>
|
||||
<br>
|
||||
A <strong>.gov</strong> website belongs to an official government organization in the United States.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-https.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Secure .gov websites use HTTPS</strong>
|
||||
<br>
|
||||
A <strong>lock</strong> ( <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" width="52" height="64" viewBox="0 0 52 64" class="usa-banner__lock-image" role="img" aria-labelledby="banner-lock-description" focusable="false"><title id="banner-lock-title">Lock</title><desc id="banner-lock-description">Locked padlock</desc><path fill="#000000" fill-rule="evenodd" d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"></path></svg></span> ) or <strong>https://</strong> means you’ve safely connected to the .gov website. Share sensitive information only on official, secure websites.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -6,8 +6,8 @@ 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` and `content_template`.
|
||||
- **/layouts**: Contains base templates and shared layouts used across the site. Simply put, it defines the overall structure or skeleton of the application (less frequently revised).
|
||||
- `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`.
|
||||
- **/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
|
||||
- **/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)
|
||||
@@ -18,11 +18,11 @@ 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,49 +1,7 @@
|
||||
{% from "components/pill.html" import pill %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
{% if notifications_deleted %}
|
||||
<div class="grid-row ">
|
||||
{% for label, query_param, url, count in counts %}
|
||||
{% if query_param == 'pending' %}
|
||||
<div class="grid-col-3">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ query_param }}</span>
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid-col-3">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="tabs">
|
||||
{{ pill(counts, request.args.get('status', '')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="tabs">
|
||||
{{ pill(counts, request.args.get('status', '')) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
{% set display_message_status %}
|
||||
{% if job.template.content %}
|
||||
<h2 class="sms-message-header">Message</h2>
|
||||
<div class="sms-message-wrapper">{{ current_service.name }}: {{job.template.content}}</div>
|
||||
{% endif %}
|
||||
{% if job.original_file_name %}
|
||||
<h2 class="recipient-list">Recipient list</h2>
|
||||
<div>
|
||||
<ul class="usa-icon-list">
|
||||
<li class="usa-icon-list__item">
|
||||
<img src="{{ url_for('static', filename='img/material-icons/description.svg') }}" alt="Description Icon">
|
||||
<div class="usa-icon-list__content">
|
||||
<h3>{{ job.original_file_name }}</h3>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2>Delivery Status</h2>
|
||||
{% endset %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
<p class='bottom-gutter'>
|
||||
{% if job.scheduled_for %}
|
||||
{% if job.processing_started %}
|
||||
Sent by {{ job.created_by.name }} on {{ job.processing_started|format_datetime_short_america }}
|
||||
{% if job.still_processing or arrived_from_preview_page_url %}
|
||||
{% if job.scheduled_for %}
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Your text has been scheduled</h4>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{display_message_status}}
|
||||
{% else %}
|
||||
Uploaded by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short_america }}
|
||||
<div class="usa-alert usa-alert--success">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Your text has been sent</h4>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }} was sent on {{ job.created_at|format_datetime_normal }} by {{ job.created_by.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{display_message_status}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Sent by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short_america }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if job.status == 'sending limits exceeded'%}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endcall %}
|
||||
{% if item.expiry_date %}
|
||||
{% call field(align='right') %}
|
||||
<span class='hint'>Revoked {{ item.expiry_date|format_datetime_short }}</span>
|
||||
<span class='hint'>Revoked {{ item.expiry_date|format_datetime_table }}</span>
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{% call field(align='right', status='error') %}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
{{ page_header('Preview') }}
|
||||
<div>
|
||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for if scheduled_for else 'Now'}}</p>
|
||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_normal if scheduled_for else 'Now'}}</p>
|
||||
<p class="sms-message-file-name">File: {{original_file_name}}</p>
|
||||
<p class="sms-message-template">Template: {{template.name}}</p>
|
||||
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
||||
@@ -42,25 +42,55 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="usa-table-container--scrollable" tabindex="0">
|
||||
{% call(item, row_number) list_table(
|
||||
recipients.displayed_rows,
|
||||
caption="Note: Only the first 5 rows are displayed here.",
|
||||
caption_visible=True,
|
||||
field_headings=recipients.column_headers
|
||||
) %}
|
||||
{% for column in recipients.column_headers %}
|
||||
{% if item[column].ignore %}
|
||||
{{ text_field(item[column].data or '', status='default') }}
|
||||
{% else %}
|
||||
{{ text_field(item[column].data or '') }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if item[None].data %}
|
||||
{% for column in item[None].data %}
|
||||
{{ text_field(column, status='default') }}
|
||||
<table class="usa-table usa-table--borderless width-full">
|
||||
<caption class="font-body-lg table-heading">
|
||||
Note: Only the first 5 rows are displayed here.
|
||||
</caption>
|
||||
<thead class="table-field-headings-visible">
|
||||
<tr>
|
||||
{% for header in recipients.column_headers %}
|
||||
<th scope="col" class="table-field-heading-first">
|
||||
{{ header }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in recipients.displayed_rows %}
|
||||
{% if loop.index <= 5 %}
|
||||
<tr class="table-row">
|
||||
{% for column in recipients.column_headers %}
|
||||
<td class="table-field-left-aligned">
|
||||
<div>
|
||||
{% set column_data = item[column].data or '' %}
|
||||
{% if column_data is iterable and column_data is not string %}
|
||||
<ul>
|
||||
{% for data_item in column_data %}
|
||||
{% if data_item is not none %}
|
||||
<li>{{ data_item }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ column_data }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% if item[None].data %}
|
||||
{% for column in item[None].data %}
|
||||
<td class="table-field-left-aligned">
|
||||
<div class="">
|
||||
{{ column }}
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- <div class="bottom-gutter-3-2">
|
||||
|
||||
@@ -1,43 +1,25 @@
|
||||
<div class="ajax-block-container">
|
||||
<div class="grid-row grid-gap">
|
||||
<div id="total-sms" class="grid-col-12">
|
||||
<span class="big-number-with-status">
|
||||
<a class="usa-link display-flex" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='sending,delivered,failed') }}">
|
||||
<div id="total-sms" class="grid-col-12 margin-top-2">
|
||||
<span class="big-number-with-status display-block margin-bottom-2">
|
||||
<p>
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if statistics['sms']['requested'] is number %}
|
||||
{{ "{:,}".format(statistics['sms']['requested']) }}
|
||||
{{ "{:,}".format(statistics['sms']['requested']) }}
|
||||
{% else %}
|
||||
{{ statistics['sms']['requested'] }}
|
||||
{{ statistics['sms']['requested'] }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ statistics['sms']['requested']|message_count_label('sms', suffix='sent') }}</span>
|
||||
<span class="big-number-label">{{ statistics['sms']['requested']|message_count_label('sms', suffix='sent') }} in the last seven days</span>
|
||||
</span>
|
||||
</p>
|
||||
<a class="usa-button usa-button--outline" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='sending,delivered,failed') }}">
|
||||
Details
|
||||
</a>
|
||||
{% if show_failures %}
|
||||
<span class="big-number-status{% if statistics['sms']['show_warning'] is sameas true %} big-number-status--failing{% endif %}">
|
||||
{% if statistics['sms']['failed'] %}
|
||||
<a class="usa-link" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='failed') }}">
|
||||
{{ "{:,}".format(statistics['sms']['failed']) }}
|
||||
failed – {{ statistics['sms']['failed_percentage'] }}%
|
||||
</a>
|
||||
{% else %}
|
||||
No failures
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{# Removing the failures area for now, as the user can click on the above link to see all the details.
|
||||
In the future state of the dashboard, the all statuses will be more apparent with data visualizations #}
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div id="total-email" class="grid-col-6">
|
||||
<span class="big-number-with-status pilot-disabled">
|
||||
<a class="usa-link display-block margin-bottom-1">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">0</span>
|
||||
<span class="big-number-label">emails sent</span>
|
||||
</span>
|
||||
</a>
|
||||
<span class="big-number-status">No failures</span>
|
||||
</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Usage</th>
|
||||
<th scope="col">Sent</th>
|
||||
<th scope="col">Remaining</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'upcoming') }}
|
||||
|
||||
<h2 class="font-body-xl margin-0">
|
||||
<h2 class="font-body-2xl line-height-sans-2 margin-0">
|
||||
Messages sent
|
||||
</h2>
|
||||
<p class="margin-top-0">In the last seven days</p>
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||
|
||||
@@ -68,7 +67,7 @@
|
||||
{{ notification.template.name }}
|
||||
</td>
|
||||
<td class="table-field time-sent">
|
||||
{{ job.created_at | format_datetime_short_america }}
|
||||
{{ job.created_at | format_datetime_table }}
|
||||
</td>
|
||||
<td class="table-field sender">
|
||||
{{ notification.created_by.name }}
|
||||
@@ -96,36 +95,16 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Usage</h2>
|
||||
<h3 class="margin-bottom-0">Daily</h3>
|
||||
<p class="margin-0">Across all services</p>
|
||||
<table class="usage-table usa-table usa-table--borderless margin-top-1 margin-bottom-5">
|
||||
<caption class="usa-sr-only">
|
||||
Daily
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Usage</th>
|
||||
<th scope="col">Remaining</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ global_message_limit - daily_global_messages_remaining }}</td>
|
||||
<td>
|
||||
{{ daily_global_messages_remaining }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Message count</h2>
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
<h3 class='margin-bottom-0' id="current-year"></h3>
|
||||
{{ ajax_block(partials, updates_url, 'usage') }}
|
||||
<a
|
||||
href="{{ url_for('.usage', service_id=current_service['id']) }}"
|
||||
class="usa-link show-more"
|
||||
><span>See all usage</span></a>
|
||||
<p class="margin-top-0">During the pilot period, each service has an allowance of 250,000 message parts. Once this allowance is met, the
|
||||
application will stop delivering messages. There's no monthly charge, no setup fee, and no procurement cost.</p>
|
||||
<p class="align-with-heading-copy">
|
||||
What counts as 1 text message part?<br />
|
||||
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,26 +5,13 @@
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ job.original_file_name }}
|
||||
{{ "Message status" }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(job.original_file_name) }}
|
||||
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
data-key="status"
|
||||
data-form=""
|
||||
>
|
||||
{% endif %}
|
||||
{{ partials['status']|safe }}
|
||||
{% if not job.processing_finished %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
{{ partials['status']|safe }}
|
||||
{% if not finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
{{ page_header('Preview') }}
|
||||
{% endif %}
|
||||
<div>
|
||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for if scheduled_for else 'Now'}}</p>
|
||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_normal if scheduled_for else 'Now'}}</p>
|
||||
<p class="sms-message-template">Template: {{template.name}}</p>
|
||||
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
||||
<p class="sms-message-sender" >To: {{ recipient }}</p>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
{{ text_field(item.complaint_type) }}
|
||||
|
||||
{{ text_field(item.complaint_date|format_datetime_short if item.complaint_date else None) }}
|
||||
{{ text_field(item.complaint_date|format_datetime_table if item.complaint_date else None) }}
|
||||
|
||||
{% endcall %}
|
||||
|
||||
|
||||
77
app/templates/views/set-up-your-profile.html
Normal file
77
app/templates/views/set-up-your-profile.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/components/select/macro.njk" import usaSelect -%}
|
||||
|
||||
{% block per_page_title %}
|
||||
Set up your profile
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-8">
|
||||
<h1 class="font-body-2xl margin-bottom-3">Set up your profile</h1>
|
||||
{% call form_wrapper(autocomplete=True) %}
|
||||
{{ form.name(param_extensions={}) }}
|
||||
<div class="extra-tracking">
|
||||
{{ form.mobile_number(param_extensions={
|
||||
"hint": {"text": "We'll send you a security code by text message"},
|
||||
}) }}
|
||||
</div>
|
||||
<!--{{ usaSelect({
|
||||
"id": "time-zone",
|
||||
"name": "time-zone",
|
||||
"label": "Time zone",
|
||||
"items": [
|
||||
{
|
||||
"value": "",
|
||||
"disabled": true,
|
||||
"text": "- Select -"
|
||||
},
|
||||
{
|
||||
"value": "America/Puerto_Rico",
|
||||
"text": "America/Puerto_Rico"
|
||||
},
|
||||
{
|
||||
"value": "US/Eastern",
|
||||
"text": "US/Eastern"
|
||||
},
|
||||
{
|
||||
"value": "US/Central",
|
||||
"text": "US/Central"
|
||||
},
|
||||
{
|
||||
"value": "US/Mountain",
|
||||
"text": "US/Mountain"
|
||||
},
|
||||
{
|
||||
"value": "US/Pacific",
|
||||
"text": "US/Pacific"
|
||||
},
|
||||
{
|
||||
"value": "US/Alaska",
|
||||
"text": "US/Alaska"
|
||||
},
|
||||
{
|
||||
"value": "US/Hawaii",
|
||||
"text": "US/Hawaii"
|
||||
},
|
||||
{
|
||||
"value": "US/Aleutian",
|
||||
"text": "US/Aleutian"
|
||||
},
|
||||
{
|
||||
"value": "US/Samoa",
|
||||
"text": "US/Samoa"
|
||||
},
|
||||
]
|
||||
})
|
||||
}}-->
|
||||
{{form.auth_type}}
|
||||
{{ page_footer("Save") }}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -21,7 +21,7 @@ Notify.gov
|
||||
<h1 class="font-serif-2xl usa-hero__heading">Reach people where they are with government-powered text messages</h1>
|
||||
<p class="font-sans-lg">Notify.gov is a text message service that helps federal, state, local, tribal and territorial governments more effectively communicate with the people they serve.</p>
|
||||
<div class="usa-button-group margin-bottom-5">
|
||||
<a class="usa-button usa-button--big margin-right-2" href="{{ url_for('main.sign_in' )}}">Sign in</a>
|
||||
<a class="usa-button usa-button--big margin-right-2" href="{{ url_for('main.sign_in' ) }}">Sign in</a>
|
||||
if you are an existing pilot partner
|
||||
</div>
|
||||
<p class="font-sans-md">Currently we are only working with select pilot partners. If you are interested in using Notify.gov in the future, please contact <br><a href="mailto:tts-benefits-studio@gsa.gov">tts-benefits-studio@gsa.gov</a> to learn more.</p>
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
<div class='grid-row'>
|
||||
|
||||
<div class='grid-col-12'>
|
||||
<h2 class="heading-small margin-bottom-1">Daily messages across all services</h2>
|
||||
<p class="margin-0">You have sent {{ global_message_limit - daily_global_messages_remaining }} of your {{ global_message_limit }} daily messages allowance.</p>
|
||||
<p class="margin-0"></p>You have {{ daily_global_messages_remaining }} messages remaining.</p>
|
||||
<h2 class='heading-small margin-bottom-1'>Text message parts</h2>
|
||||
<div class="keyline-block">
|
||||
You have sent
|
||||
@@ -82,8 +79,8 @@
|
||||
empty_message='',
|
||||
field_headings=[
|
||||
'By month',
|
||||
hidden_field_heading('Text messages'),
|
||||
hidden_field_heading('Cost'),
|
||||
'Text message parts',
|
||||
'Total message allowance',
|
||||
],
|
||||
field_headings_visible=True
|
||||
) %}
|
||||
@@ -109,14 +106,9 @@
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% call field(align='left') %}
|
||||
{{ big_number(
|
||||
item.sms_cost,
|
||||
currency="$",
|
||||
smallest=True
|
||||
) }}
|
||||
<ul>
|
||||
{% if item.sms_free_allowance_used %}
|
||||
<li class="tabular-numbers">{{ item.sms_free_allowance_used|format_thousands }} free {{ item.sms_free_count|message_count_label('sms', suffix='') }}</li>
|
||||
<li class="tabular-numbers">{{ item.sms_free_allowance_used|format_thousands }} total {{ item.sms_free_count|message_count_label('parts', suffix='') }}</li>
|
||||
{% endif %}
|
||||
{% for sms in item.sms_breakdown %}
|
||||
<li class="tabular-numbers">{{ sms.charged_units|message_count('sms') }} at
|
||||
|
||||
@@ -22,3 +22,8 @@ def is_less_than_days_ago(date_from_db, number_of_days):
|
||||
|
||||
def parse_naive_dt(dt):
|
||||
return parser.parse(dt, ignoretz=True)
|
||||
|
||||
|
||||
def parse_dt(dt):
|
||||
# Parse datetime without ignoring the timezone
|
||||
return parser.parse(dt)
|
||||
|
||||
Reference in New Issue
Block a user