Skip to content

Commit 8dd1ae6

Browse files
authored
Merge pull request #2533 from GSA/main
04/24/2025 Production Deploy
2 parents 52af890 + b78a430 commit 8dd1ae6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1163
-693
lines changed

.ds.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
"filename": "app/config.py",
152152
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
153153
"is_verified": false,
154-
"line_number": 118,
154+
"line_number": 120,
155155
"is_secret": false
156156
}
157157
],
@@ -674,5 +674,5 @@
674674
}
675675
]
676676
},
677-
"generated_at": "2025-03-20T18:22:36Z"
677+
"generated_at": "2025-04-10T19:38:31Z"
678678
}

.github/workflows/checks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ jobs:
167167
run: make run-flask &
168168
env:
169169
NOTIFY_ENVIRONMENT: scanning
170+
FEATURE_SOCKET_ENABLED: true
170171
- name: Run OWASP Baseline Scan
171172
uses: zaproxy/action-baseline@v0.14.0
172173
with:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*.XLSX
1515

1616
## Non user files allowed to be commited
17+
!app/assets/pdf/wa_case_study.pdf
1718
!app/assets/pdf/best-practices-for-texting-the-public.pdf
1819
!app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf
1920
!app/assets/pdf/best-practices-section-outline.pdf

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ generate-version-file: ## Generates the app version file
6969
@echo -e "__git_commit__ = \"${GIT_COMMIT}\"\n__time__ = \"${DATE}\"" > ${APP_VERSION_FILE}
7070

7171
.PHONY: test
72-
test: py-lint py-test js-lint js-test ## Run tests
72+
test: py-lint py-test js-test ## Run tests
7373

7474
.PHONY: py-lint
7575
py-lint: ## Run python linting scanners and black
@@ -99,7 +99,7 @@ too-complex:
9999
py-test: export NEW_RELIC_ENVIRONMENT=test
100100
py-test: ## Run python unit tests
101101
poetry run coverage run -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
102-
poetry run coverage report --fail-under=96
102+
poetry run coverage report --fail-under=93
103103
poetry run coverage html -d .coverage_cache
104104

105105
.PHONY: dead-code

app/__init__.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
from app.notify_client.upload_api_client import upload_api_client
110110
from app.notify_client.user_api_client import user_api_client
111111
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
112+
from app.utils.api_health import is_api_down
112113
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
113114
from notifications_utils import logging, request_helper
114115
from notifications_utils.formatters import (
@@ -158,11 +159,14 @@ def _csp(config):
158159
"https://www.googletagmanager.com",
159160
"https://www.google-analytics.com",
160161
"https://dap.digitalgov.gov",
162+
"https://cdn.socket.io",
161163
],
162164
"connect-src": [
163165
"'self'",
164166
"https://gov-bam.nr-data.net",
165167
"https://www.google-analytics.com",
168+
"http://localhost:6011",
169+
"ws://localhost:6011",
166170
],
167171
"style-src": ["'self'", asset_domain],
168172
"img-src": ["'self'", asset_domain, logo_domain],
@@ -173,17 +177,22 @@ def create_app(application):
173177
@application.after_request
174178
def add_csp_header(response):
175179
existing_csp = response.headers.get("Content-Security-Policy", "")
176-
response.headers["Content-Security-Policy"] = existing_csp + "; form-action 'self';"
180+
response.headers["Content-Security-Policy"] = (
181+
existing_csp + "; form-action 'self';"
182+
)
177183
return response
178-
# @application.context_processor
179-
# def inject_feature_flags():
180-
# this is where feature flags can be easily added as a dictionary within context
181-
# feature_about_page_enabled = application.config.get(
182-
# "FEATURE_ABOUT_PAGE_ENABLED", False
183-
# )
184-
# return dict(
185-
# FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled,
186-
# )
184+
185+
@application.context_processor
186+
def inject_feature_flags():
187+
# this is where feature flags can be easily added as a dictionary within context
188+
feature_socket_enabled = application.config.get("FEATURE_SOCKET_ENABLED", False)
189+
return dict(
190+
FEATURE_SOCKET_ENABLED=feature_socket_enabled,
191+
)
192+
193+
@application.context_processor
194+
def inject_is_api_down():
195+
return {"is_api_down": is_api_down()}
187196

188197
@application.context_processor
189198
def inject_initial_signin_url():

app/assets/images/api-error.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,69 @@
1+
function announceUploadStatusFromElement() {
2+
const srRegion = document.getElementById('upload-status-live');
3+
const success = document.getElementById('upload-success');
4+
const error = document.getElementById('upload-error');
5+
6+
if (!srRegion) return;
7+
8+
const message = error?.textContent || success?.textContent;
9+
10+
if (message) {
11+
srRegion.textContent = '';
12+
setTimeout(() => {
13+
srRegion.textContent = message + '\u00A0'; // add a non-breaking space
14+
srRegion.focus(); // Optional
15+
}, 300);
16+
}
17+
}
18+
19+
20+
// Exported for use in tests
21+
function initUploadStatusAnnouncer() {
22+
document.addEventListener('DOMContentLoaded', () => {
23+
announceUploadStatusFromElement();
24+
});
25+
}
26+
127
(function(Modules) {
228
"use strict";
329

430
Modules.FileUpload = function() {
5-
631
this.submit = () => this.$form.trigger('submit');
732

8-
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
9-
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
10-
`);
33+
this.showCancelButton = () => {
34+
$('.file-upload-button', this.$form).replaceWith(`
35+
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
36+
Uploading<span class="dot-anim" aria-hidden="true"></span>
37+
</button>
38+
`);
39+
};
1140

1241
this.start = function(component) {
13-
1442
this.$form = $(component);
1543

16-
// The label gets styled like a button and is used to hide the native file upload control. This is so that
17-
// users see a button that looks like the others on the site.
44+
this.$form.on('click', '[data-module="upload-trigger"]', function () {
45+
const inputId = $(this).data('file-input-id');
46+
const fileInput = document.getElementById(inputId);
47+
if (fileInput) fileInput.click();
48+
});
1849

19-
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
20-
21-
// Clear the form if the user navigates back to the page
2250
$(window).on("pageshow", () => this.$form[0].reset());
2351

24-
// Need to put the event on the container, not the input for it to work properly
25-
this.$form.on(
26-
'change', '.file-upload-field',
27-
() => this.submit() && this.showCancelButton()
28-
);
29-
52+
this.$form.on('change', '.file-upload-field', () => {
53+
this.submit();
54+
this.showCancelButton();
55+
});
3056
};
57+
};
58+
})(window.GOVUK.Modules);
3159

60+
if (typeof module !== 'undefined' && module.exports) {
61+
module.exports = {
62+
announceUploadStatusFromElement,
63+
initUploadStatusAnnouncer
3264
};
65+
}
3366

34-
})(window.GOVUK.Modules);
67+
if (typeof window !== 'undefined') {
68+
initUploadStatusAnnouncer();
69+
}

app/assets/javascripts/fullscreenTable.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
this.$scrollableTable
2323
.on('scroll', this.toggleShadows)
2424
.on('scroll', this.maintainHeight)
25-
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
2625

2726
if (
2827
window.GOVUK.stickAtBottomWhenScrolling &&
@@ -37,11 +36,11 @@
3736

3837
this.insertShims = () => {
3938

40-
const attributesForFocus = 'role aria-labelledby tabindex';
39+
const attributesForFocus = 'role aria-labelledby';
4140
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
4241

4342
this.$table.find('caption').attr('id', captionId);
44-
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`);
43+
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
4544

4645
this.$component
4746
.append(

app/assets/javascripts/socketio.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
function debounce(func, wait) {
2+
let timeout;
3+
return function (...args) {
4+
clearTimeout(timeout);
5+
timeout = setTimeout(() => func.apply(this, args), wait);
6+
};
7+
}
8+
9+
document.addEventListener('DOMContentLoaded', function () {
10+
const isJobPage = window.location.pathname.includes('/jobs/');
11+
if (!isJobPage) return;
12+
13+
const jobEl = document.querySelector('[data-job-id]');
14+
const jobId = jobEl?.dataset?.jobId;
15+
const featureEnabled = jobEl?.dataset?.feature === 'true';
16+
const apiHost = jobEl?.dataset?.host;
17+
18+
if (!jobId) return;
19+
20+
if (featureEnabled) {
21+
const socket = io(apiHost);
22+
23+
socket.on('connect', () => {
24+
socket.emit('join', { room: `job-${jobId}` });
25+
});
26+
27+
window.addEventListener('beforeunload', () => {
28+
socket.emit('leave', { room: `job-${jobId}` });
29+
});
30+
31+
const debouncedUpdate = debounce((data) => {
32+
updateAllJobSections();
33+
}, 1000);
34+
35+
socket.on('job_updated', (data) => {
36+
if (data.job_id !== jobId) return;
37+
debouncedUpdate(data);
38+
});
39+
}
40+
41+
function updateAllJobSections() {
42+
const resourceEl = document.querySelector('[data-socket-update="status"]');
43+
const url = resourceEl?.dataset?.resource;
44+
45+
if (!url) {
46+
console.warn('No resource URL found for job updates');
47+
return;
48+
}
49+
50+
fetch(url)
51+
.then((res) => res.json())
52+
.then(({ status, counts, notifications }) => {
53+
const sections = {
54+
status: document.querySelector('[data-socket-update="status"]'),
55+
counts: document.querySelector('[data-socket-update="counts"]'),
56+
notifications: document.querySelector(
57+
'[data-socket-update="notifications"]'
58+
),
59+
};
60+
61+
if (status && sections.status) {
62+
sections.status.innerHTML = status;
63+
}
64+
if (counts && sections.counts) {
65+
sections.counts.innerHTML = counts;
66+
}
67+
if (notifications && sections.notifications) {
68+
sections.notifications.innerHTML = notifications;
69+
}
70+
})
71+
.catch((err) => {
72+
console.error('Error fetching job update partials:', err);
73+
});
74+
}
75+
});

app/assets/pdf/wa_case_study.pdf

1.38 MB
Binary file not shown.

0 commit comments

Comments
 (0)