merge from main

This commit is contained in:
Kenneth Kehl
2023-09-19 10:31:33 -07:00
39 changed files with 583 additions and 219 deletions

View File

@@ -2,6 +2,7 @@ import os
import pathlib
from functools import partial
from time import monotonic
from urllib.parse import urlparse, urlunparse
import jinja2
from flask import (
@@ -36,7 +37,6 @@ from werkzeug.local import LocalProxy
from app import proxy_fix
from app.asset_fingerprinter import asset_fingerprinter
from app.config import configs
from app.custom_auth import CustomBasicAuth
from app.extensions import redis_client, zendesk_client
from app.formatters import (
convert_to_boolean,
@@ -121,7 +121,6 @@ from app.url_converters import (
login_manager = LoginManager()
csrf = CSRFProtect()
talisman = Talisman()
basic_auth = CustomBasicAuth()
# The current service attached to the request stack.
@@ -234,8 +233,6 @@ def create_app(application):
login_manager.session_protection = None
login_manager.anonymous_user = AnonymousUser
setup_basic_auth(application)
# make sure we handle unicode correctly
redis_client.redis_store.decode_responses = True
@@ -254,6 +251,7 @@ def create_app(application):
def init_app(application):
application.before_request(redirect_notify_to_beta)
application.before_request(load_service_before_request)
application.before_request(load_organization_before_request)
application.before_request(request_helper.check_proxy_header_before_request)
@@ -332,6 +330,23 @@ def make_session_permanent():
session.permanent = True
def create_beta_url(url):
url_created = urlparse(url)
url_list = list(url_created)
url_list[1] = "beta.notify.gov"
url_for_redirect = urlunparse(url_list)
return url_for_redirect
def redirect_notify_to_beta():
if (
current_app.config["NOTIFY_ENVIRONMENT"] == "production"
and "beta.notify.gov" not in request.url
):
url_to_beta = create_beta_url(request.url)
return redirect(url_to_beta, 302)
def load_service_before_request():
if "/static/" in request.url:
request_ctx.service = None
@@ -565,7 +580,3 @@ def init_jinja(application):
]
jinja_loader = jinja2.FileSystemLoader(template_folders)
application.jinja_loader = jinja_loader
def setup_basic_auth(application):
application.basic_auth = CustomBasicAuth(application)

View File

@@ -16,7 +16,7 @@
// The label gets styled like a button and is used to hide the native file upload control. This is so that
// users see a button that looks like the others on the site.
//
this.$form.find('label.file-upload-button').addClass('usa-button');
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1');
// Clear the form if the user navigates back to the page
$(window).on("pageshow", () => this.$form[0].reset());

View File

@@ -9,48 +9,48 @@
let states = {
'initial': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column">
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
</div>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column">
<div class="radio-select__column margin-y-2">
{{#categories}}
<input type='button' class='govuk-button govuk-button--secondary radio-select__button--category' aria-expanded="false" value='{{.}}' />
<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='{{.}}' />
{{/categories}}
</div>
`),
'choose': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column">
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
</div>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column">
<div class="radio-select__column margin-y-2">
{{#choices}}
<div class="usa-radio js-option">
<input class="usa-radio__input" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
</div>
{{/choices}}
<input type='button' class='govuk-button govuk-button--secondary radio-select__button--done' aria-expanded='true' value='Done' />
<input type='button' class='usa-button usa-button--outline radio-select__button--done' aria-expanded='true' value='Done' />
</div>
`),
'chosen': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column">
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
</div>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column">
<div class="radio-select__column margin-y-2">
{{#choices}}
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
@@ -58,8 +58,8 @@
</div>
{{/choices}}
</div>
<div class="radio-select__column">
<input type='button' class='govuk-button govuk-button--secondary radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
<div class="radio-select__column margin-y-2">
<input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
</div>
`)
};

View File

@@ -0,0 +1,342 @@
@use "uswds-core" as *;
// SMS Box Style
.sms-message-wrapper {
position: relative;
width: 100%;
max-width: 464px;
box-sizing: border-box;
padding: units(1);
background: color('gray-cool-10');
border: 1px solid color('gray-cool-10');
border-radius: 5px;
white-space: normal;
margin: 0 0 units(1) 0;
clear: both;
word-wrap: break-word;
&:after {
content: "";
display: block;
position: absolute;
bottom: -5px;
right: -20px;
border: 10px solid transparent;
border-left-width: 13px;
border-right-width: 13px;
border-bottom-color: color('gray-cool-10');
border-left-color: color('gray-cool-10');
transform: rotate(17deg);
}
}
.sms-message-inbound {
.sms-message-wrapper {
&:after {
border-left-color: transparent;
border-bottom-color: color('gray-cool-10');
border-right-color: color('gray-cool-10');
right: auto;
left: -20px;
transform: rotate(17deg);
}
}
}
.sms-message-sender {
margin: units(1) 0 0;
}
.sms-message-recipient {
color: color('gray-cool-90');
margin: 0 0 units(1);
}
.sms-message-status {
color: color('gray-cool-90');
margin: -20px units(1) 20px units(1);
}
.sms-message-status-outbound {
text-align: right;
}
.sms-message-row {
&:focus {
outline: none;
padding-top: 120px;
margin-top: -120px;
}
}
.sms-message-reply-link {
text-align: right;
}
// File upload area
.page-footer {
position: relative;
margin-bottom: 30px;
&-link {
line-height: 40px;
padding: 1px 0 0 15px;
font-weight: normal;
&:first-of-type {
padding-left: 0px;
}
}
&-delete-link-without-button {
padding: 0;
display: inline-block;
}
&-secondary-link {
display: block;
margin-top: units(1);
}
&-right-aligned-link {
position: absolute;
right: 0;
top: 9px; // align baseline with buttons
&-without-button {
position: absolute;
right: 0;
top: 0;
}
}
&__button {
margin-right: 10px;
}
.js-cancel {
margin: 0;
}
&__button--centred {
display: block;
margin: 0 auto;
}
}
.file-upload {
&-label,
&-button-label {
font-weight: bold;
font-size: 19px;
display: block;
margin: 0 0 10px 0;
}
&-label .error-message,
&-button-label.error-message {
padding: 0;
}
&-field {
margin-bottom: 10px;
}
// Hide normal upload form if we're adding a custom version with JS
.js-enabled &-label,
.js-enabled &-field,
.js-enabled &-submit {
display: none;
}
&-alternate-link {
display: inline-block;
line-height: 35px;
a {
font-weight: bold;
}
}
}
.spreadsheet {
margin-bottom: units (1);
.table {
margin-bottom: 0;
}
.usa-table--borderless thead th {
border-top: 1px solid color('gray-cool-10');
}
th,
.table-field-index {
background: color('gray-cool-10');
border: 1px solid color('gray-cool-10');
font-weight: bold;
text-align: center;
}
th, td {
padding-left: 10px;
padding-right: 10px;
border: 1px solid color('gray-cool-10');
}
td {
border-top: 0;
// 194 is the width of the table * 1/3.5, so the overflow cuts off
// at 3.5 columns wide.
// 11 accounts for the padding of the table cell
min-width: 194px - 11px;
&:first-child {
min-width: auto;
}
}
.fullscreen-fixed-table {
z-index: 1000;
.table-field-heading-first {
background: color('gray-cool-10');
}
}
}
.fullscreen {
&-content {
background: #FFFFFF;
z-index: 10;
overflow-y: hidden;
box-sizing: border-box;
margin: 0 0 units(1) 0;
padding: 0 0 0 0;
overflow: hidden;
border-bottom: 1px solid color('gray-cool-10');
.table {
margin-bottom: 0;
tr:last-child {
td {
border-bottom: 1px solid #FFFFFF;
}
}
}
th,
.table-field-error-label,
.table-field-left-aligned {
white-space: nowrap;
}
}
&-right-shadow {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
z-index: 200;
&.visible {
&.with-transition {
transition: box-shadow 0.6s ease-out;
}
box-shadow: inset -1px 0 0 0 color('gray-cool-10'), inset -3px 0 0 0 rgba(color('gray-cool-10'), 0.2);
}
}
&-scrollable-table {
overflow-x: auto;
overflow-y: hidden;
.table-field-heading-first,
.table-field-index {
display: none;
}
.table-field-left-aligned {
position: relative;
z-index: 150;
background: #FFFFFF;
}
&::-webkit-scrollbar {
-webkit-appearance: none;
}
&::-webkit-scrollbar:horizontal {
height: 11px;
background-color: #FFFFFF;
}
&::-webkit-scrollbar-thumb {
border-radius: 8px;
border: 2px solid #FFFFFF;
background-color: rgba(0, 0, 0, .5);
}
&::-webkit-scrollbar-track {
background-color: #FFFFFF;
border-radius: 8px;
}
}
&-fixed-table {
position: absolute;
top: 0;
overflow: hidden;
.table-field-heading {
visibility: hidden;
}
.table-field-left-aligned {
width: 0;
position: relative;
z-index: 100;
visibility: hidden;
}
.table-field-heading-first,
.table-field-index {
transition: none;
position: relative;
z-index: 200;
background: #FFFFFF;
}
}
&-scrolled-table {
padding-bottom: 20px;
.table-field-heading-first,
.table-field-index {
transition: box-shadow 0.3s ease-in-out;
box-shadow: 1px 0 0 0 color('gray-cool-10'), 3px 0 0 0 rgba(color('gray-cool-10'), 0.2);
}
}
&-shim {
width: 100%;
position: relative;
z-index: 9;
margin-bottom: 30px;
pointer-events: none;
& + .table-show-more-link {
margin-top: -20px;
}
}
}
.js-focus-style {
outline: 3px solid color("blue-40v");
box-shadow: 0 0 0 7px color("blue-40v");
*:focus {
outline: none;
}
}

View File

@@ -33,10 +33,19 @@ i.e.
}
}
.usa-nav__primary > .usa-nav__primary-item > a {
font-size: size("body", 4);
.usa-nav__primary {
&> .usa-nav__primary-item > a {
font-size: size("body", 4);
}
&> .usa-nav__primary-item:last-child {
margin-left: auto;
@include u-margin-right(-4);
}
}
.usa-nav__primary
h1 {
font-weight: bold !important;
}
@@ -250,6 +259,9 @@ td.table-empty-message {
background: color("green-cool-40v");
display: flex;
padding: units(1) units(2);
&--failing {
background: color("red-warm-50v");
}
}
}
.usa-table {
@@ -443,16 +455,3 @@ details form {
padding-left: 5px;
letter-spacing: 0.04em;
}
// .textbox-colour-preview {
// @include govuk-media-query($from: desktop) {
// width: 38px;
// height: 38px;
// margin-left: 5px;
// border-radius: 50%;
// box-shadow: inset 0 0 0 1px rgba(govuk-colour("black"), 0.2);
// display: inline-block;
// vertical-align: top;
// transition: background 0.3s ease-out;
// }
// }

View File

@@ -1,3 +1,4 @@
@forward "uswds-theme";
@forward "uswds";
@forward "uswds-theme-custom-styles";
@forward "uswds-theme-custom-styles";
@forward "legacy-styles";

View File

@@ -28,8 +28,6 @@ class Config(object):
# ZENDESK_API_KEY = getenv('ZENDESK_API_KEY')
ROUTE_SECRET_KEY_1 = getenv("ROUTE_SECRET_KEY_1", "dev-route-secret-key-1")
ROUTE_SECRET_KEY_2 = getenv("ROUTE_SECRET_KEY_2", "dev-route-secret-key-2")
BASIC_AUTH_USERNAME = getenv("BASIC_AUTH_USERNAME")
BASIC_AUTH_PASSWORD = getenv("BASIC_AUTH_PASSWORD")
NR_ACCOUNT_ID = getenv("NR_ACCOUNT_ID")
NR_TRUST_KEY = getenv("NR_TRUST_KEY")
@@ -101,7 +99,6 @@ def _s3_credentials_from_env(bucket_prefix):
class Development(Config):
BASIC_AUTH_FORCE = False
DEBUG = True
SESSION_COOKIE_SECURE = False
SESSION_PROTECTION = None
@@ -138,7 +135,6 @@ class Test(Development):
class Production(Config):
HEADER_COLOUR = "#005EA5" # $govuk-blue
HTTP_PROTOCOL = "https"
BASIC_AUTH_FORCE = True
ASSET_DOMAIN = "" # TODO use a CDN
ASSET_PATH = "/static/" # TODO use a CDN
DEBUG = False
@@ -153,7 +149,6 @@ class Production(Config):
class Staging(Production):
BASIC_AUTH_FORCE = True
HEADER_COLOUR = "#00ff00" # $green
@@ -166,7 +161,6 @@ class Sandbox(Staging):
class Scanning(Production):
BASIC_AUTH_FORCE = False
HTTP_PROTOCOL = "http"
API_HOST_NAME = "https://notify-api-staging.app.cloud.gov/"
SECRET_KEY = "dev-notify-secret-key" # nosec B105 - only used in development

View File

@@ -1,18 +0,0 @@
from flask import jsonify, request
from flask_basicauth import BasicAuth
class CustomBasicAuth(BasicAuth):
"""
Description:
Override BasicAuth to permit anonymous healthcheck at /_status?simple=true
"""
def challenge(self):
if "/_status" in request.url:
if request.args.get("elb", None) or request.args.get("simple", None):
return jsonify(status="ok"), 200
return super(CustomBasicAuth, self).challenge()
custom_basic_auth = CustomBasicAuth()

View File

@@ -39,7 +39,7 @@
<span class="big-number-with-status">
{{ big_number(number, label, link=link, smaller=smaller, smallest=smallest) }}
{% if show_failures %}
<span class="big-number-status{% if danger_zone %}-failing{% endif %}">
<span class="big-number-status{% if danger_zone %} big-number-status--failing{% endif %}">
{% if failures %}
{% if failure_link %}
<a class="usa-link" href="{{ failure_link }}">

View File

@@ -6,7 +6,7 @@
link_current_item=False,
root_element='h1'
) %}
<{{ root_element }} class="font-body-lg folder-heading"{% if root_element == 'h1' %} id="page-header"{% endif %}>
<{{ root_element }} class="font-body-lg folder-heading margin-bottom-0"{% if root_element == 'h1' %} id="page-header"{% endif %}>
{% for folder in folders %}
{% if loop.last and not link_current_item %}
{% if folder.template_type or not folder.id %}

View File

@@ -1,6 +1,6 @@
{% macro status_box(number, label, failing=false, percentage=None, url=None) %}
<div class="big-number-with-status">
<div class="big-number-status{% if failing %}-failing{% endif %}">
<div class="big-number-status{% if failing %} big-number-status--failing{% endif %}">
{% if url %}
<a class="usa-link" href="{{ url }}">{{ number }} {{ label }}</a>
{% else %}

View File

@@ -3,7 +3,7 @@
<div class="ajax-block-container">
{% if notifications_deleted %}
<div class="grid-row bottom-gutter-1-2">
<div class="grid-row ">
{% for label, query_param, url, count in counts %}
{% if query_param == 'pending' %}
<div class="grid-col-3">{{ big_number(count, query_param, smaller=True) }}</div>
@@ -14,7 +14,7 @@
{% endfor %}
</div>
{% else %}
<div class="bottom-gutter">
<div class="tabs">
{{ pill(counts, request.args.get('status', '')) }}
</div>
{% endif %}

View File

@@ -12,7 +12,7 @@
API integration
</h1>
<nav class="grid-row bottom-gutter-1-2">
<nav class="grid-row">
<div class="grid-col-4">
<a class="usa-link pill-separate-item" href="{{ url_for('.api_keys', service_id=current_service.id) }}">API keys</a>
</div>

View File

@@ -15,7 +15,7 @@
{% block maincolumn_content %}
<div class="bottom-gutter-1-2">
<div class="">
{% call banner_wrapper(type='dangerous') %}
{% if recipients.too_many_rows %}

View File

@@ -21,11 +21,6 @@
{{ page_header('Preview of {}'.format(template.name)) }}
{{ usaSkipLink({
"text": "Skip to file contents",
"href": "#" + file_contents_header_id
}) }}
{{ template|string }}
<div class="bottom-gutter-3-2">
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id)}}" class='page-footer'>

View File

@@ -4,6 +4,7 @@
{% from "components/table.html" import mapping_table, row, field, text_field, index_field, hidden_field_heading %}
{% from "components/file-upload.html" import file_upload %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/components/alert/macro.njk" import usaAlert %}
{% block service_page_title %}
Error
@@ -15,27 +16,31 @@
{% block maincolumn_content %}
<div class="bottom-gutter-1-2">
<div class="">
{% call banner_wrapper(type='dangerous') %}
{% if row_errors|length == 1 %}
<h1 class='banner-title' data-module="track-error" data-error-type="Bad rows" data-error-label="{{ upload_id }}">
Theres a problem with {{ original_file_name }}
</h1>
<p>
You need to {{ row_errors[0] }}.
</p>
<div class="usa-alert usa-alert--error" role="alert">
<div class="usa-alert__body">
<h1 class="usa-alert__heading">Theres a problem with {{ original_file_name }}</h1>
<p class="usa-alert__text">
You need to {{ row_errors[0] }}.
</p>
</div>
</div>
{% else %}
<h1 class='banner-title' data-module="track-error" data-error-type="Bad rows" data-error-label="{{ upload_id }}">
There are some problems with {{ original_file_name }}
</h1>
<p>
You need to:
</p>
<ul class="list-bullet">
{% for error in row_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
<div class="usa-alert usa-alert--error" role="alert">
<div class="usa-alert__body">
<h1 class="usa-alert__heading">Theres a problem with {{ original_file_name }}</h1>
<p class="usa-alert__text">
You need to:
<ul class="list-bullet">
{% for error in row_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</p>
</div>
</div>
{% endif %}
{% endcall %}
</div>
@@ -49,7 +54,6 @@
button_text='Upload your file again'
) }}
</div>
<a href="#content" class="usa-link back-to-top-link">Back to top</a>
</div>
<div class="fullscreen-content" data-module="fullscreen-table">

View File

@@ -3,10 +3,10 @@
<div class="ajax-block-container">
{% if current_service.scheduled_job_stats.count %}
<h2 class="heading-medium heading-upcoming-jobs">
<h2 class="font-body-lg margin-y-0">
In the next few days
</h2>
<a class="usa-link banner-dashboard" href="{{ url_for('.uploads', service_id=current_service.id) }}">
<a class="usa-link banner-dashboard margin-bottom-2 display-block" href="{{ url_for('.uploads', service_id=current_service.id) }}">
<span class="banner-dashboard-count">
{{ current_service.scheduled_job_stats.count }}
</span>
@@ -18,7 +18,7 @@
{% endif %}
</span>
<span class="banner-dashboard-meta">
sending starts
- sending starts
{{ current_service.scheduled_job_stats.soonest_scheduled_for|format_datetime_relative }}
</span>
</a>

View File

@@ -19,7 +19,7 @@
{{ ajax_block(partials, updates_url, 'upcoming') }}
<h2 class="font-body-lg margin-top-0">
<h2 class="font-body-lg margin-top-0 margin-bottom-1">
In the last seven days
</h2>
@@ -30,7 +30,7 @@
{{ ajax_block(partials, updates_url, 'template-statistics') }}
{% if current_user.has_permissions('manage_service') %}
<h2 class='font-body-lg'>This year</h2>
<h2 class='font-body-lg margin-bottom-0'>This year</h2>
{{ ajax_block(partials, updates_url, 'usage') }}
{{ show_more(
url_for(".usage", service_id=current_service['id']),

View File

@@ -28,9 +28,9 @@
{{ form.placeholder_value(param_extensions={"classes": ""}) }}
</div>
{% if skip_link or link_to_upload %}
<div class="grid-col-12">
<div class="grid-col-12 margin-top-1">
{% if link_to_upload %}
<a class="usa-link" href="{{ url_for('.send_messages', service_id=current_service.id, template_id=template.id) }}">Upload a list of {{ 999|recipient_count_label(template.template_type) }}</a>
<a class="usa-link margin-right-2" href="{{ url_for('.send_messages', service_id=current_service.id, template_id=template.id) }}">Upload a list of {{ 999|recipient_count_label(template.template_type) }}</a>
{% endif %}
{% if skip_link %}
<a href="{{ skip_link[1] }}" class="usa-link">{{ skip_link[0] }}</a>

View File

@@ -53,7 +53,7 @@
<a class="usa-link" href="{{ url_for('.get_example_csv', service_id=current_service.id, template_id=template.id) }}" download>Download this example (<abbr title="Comma separated values">CSV</abbr>)</a>
</p>
<h2 class="font-body-lg">Your file will populate this template ({{ template.name }})</h2>
<h2 class="font-body-lg margin-bottom-1">Your file will populate this template:<br><span class="font-body-lg">({{ template.name }})</span></h2>
{{ template|string }}
{% endblock %}

View File

@@ -373,7 +373,7 @@
{% endif %}
{% if (not current_service.active) and current_user.platform_admin %}
<p>
<div class="hint bottom-gutter-1-2">
<div class="hint ">
Service suspended
</div>

View File

@@ -10,7 +10,7 @@
{% block maincolumn_content %}
<div class="bottom-gutter-1-2">
<div class="">
<h1 class="font-body-2xl margin-bottom-3">Choose a template</h1>
{{ folder_path(template_folder_path, current_service.id, template_type, current_user, root_element='h2') }}
</div>

View File

@@ -10,7 +10,7 @@
{% block maincolumn_content %}
<div class="bottom-gutter-1-2">
<div class="">
<h1 class="font-body-2xl margin-bottom-3">{{ page_title }}</h1>
{{ copy_folder_path(template_folder_path, current_service.id, from_service, current_user) }}
</div>

View File

@@ -9,7 +9,7 @@
{% block maincolumn_content %}
<div class="grid-row bottom-gutter-1-2">
<div class="grid-row ">
<div class="grid-col-12">
{{ folder_path(
folders=template_folder_path,

View File

@@ -32,7 +32,7 @@
{% else %}
<div class="grid-row">
<div class="grid-col-12">
<h1 class="font-body-lg folder-heading">
<h1 class="font-body-xl folder-heading margin-top-0">
Review your message
</h1>
{{ folder_path(
@@ -70,7 +70,7 @@
{% endif %}
</div>
<!--<div class="bottom-gutter-1-2">
<!--<div class="">
{{ copy_to_clipboard(template.id, name="Template ID", thing='template ID') }}
</div>-->