Compare commits

...

266 Commits

Author SHA1 Message Date
Beverly Nguyen
7fc30ed3d8 only plain text is allowed 2025-09-18 11:29:29 -07:00
dependabot[bot]
34d6c827c2 Bump gevent from 25.8.2 to 25.9.1 (#2917)
Bumps [gevent](https://github.com/gevent/gevent) from 25.8.2 to 25.9.1.
- [Release notes](https://github.com/gevent/gevent/releases)
- [Changelog](https://github.com/gevent/gevent/blob/master/docs/changelog_pre.rst)
- [Commits](https://github.com/gevent/gevent/compare/25.8.2...25.9.1)

---
updated-dependencies:
- dependency-name: gevent
  dependency-version: 25.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-18 13:12:12 -04:00
ccostino
8b4f30874b Merge pull request #2910 from GSA/2627-debugging-socketio
Improve polling performance and remove unused   updateContent module
2025-09-17 13:24:42 -04:00
Beverly Nguyen
4fc131de66 update error handling 2025-09-17 10:00:29 -07:00
Beverly Nguyen
9d59641562 flake8 2025-09-16 22:46:35 -07:00
Beverly Nguyen
e0cfb38ea0 fix pytest 2025-09-16 22:31:07 -07:00
Beverly Nguyen
8347bee078 Added more test for 75% globally 2025-09-16 21:55:17 -07:00
dependabot[bot]
d720860eb8 Bump cryptography from 45.0.7 to 46.0.0 (#2913)
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.7 to 46.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.7...46.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-17 00:47:05 +00:00
dependabot[bot]
a36fd553e9 Bump phonenumbers from 9.0.13 to 9.0.14 (#2914)
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.13 to 9.0.14.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.13...v9.0.14)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-version: 9.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-17 00:37:10 +00:00
dependabot[bot]
f023500efc Bump pytest-mock from 3.15.0 to 3.15.1 (#2915)
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.15.0 to 3.15.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.15.0...v3.15.1)

---
updated-dependencies:
- dependency-name: pytest-mock
  dependency-version: 3.15.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 20:26:11 -04:00
Beverly Nguyen
0c347fe3db update jest test 2025-09-16 14:20:54 -07:00
dependabot[bot]
8a2516b423 Bump rollup from 4.50.1 to 4.50.2 (#2911)
Bumps [rollup](https://github.com/rollup/rollup) from 4.50.1 to 4.50.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.50.1...v4.50.2)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.50.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 09:54:52 -04:00
Beverly Nguyen
42c5d6183e fix jest testing 2025-09-15 13:37:48 -07:00
Beverly Nguyen
f765f19c3b improve polling performance and remove unused updateContent module 2025-09-15 13:16:13 -07:00
Beverly Nguyen
5ae6cf7abf removing updatecontent function and using socketio 2025-09-15 00:49:01 -07:00
Beverly Nguyen
c8be3d37bd rm updateContent.js 2025-09-15 00:44:07 -07:00
ccostino
afe62a70c1 Merge pull request #2909 from GSA/dependabot/pip/numpy-2.3.3
Bump numpy from 2.3.2 to 2.3.3
2025-09-11 17:00:10 -04:00
dependabot[bot]
6d2e0f4aa9 Bump numpy from 2.3.2 to 2.3.3
Bumps [numpy](https://github.com/numpy/numpy) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.2...v2.3.3)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 18:33:58 +00:00
ccostino
232425558b Merge pull request #2908 from GSA/dependabot/pip/pycparser-2.23
Bump pycparser from 2.22 to 2.23
2025-09-11 14:31:51 -04:00
dependabot[bot]
7004a02e96 Bump pycparser from 2.22 to 2.23
Bumps [pycparser](https://github.com/eliben/pycparser) from 2.22 to 2.23.
- [Release notes](https://github.com/eliben/pycparser/releases)
- [Changelog](https://github.com/eliben/pycparser/blob/main/CHANGES)
- [Commits](https://github.com/eliben/pycparser/compare/release_v2.22...release_v2.23)

---
updated-dependencies:
- dependency-name: pycparser
  dependency-version: '2.23'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 16:48:15 +00:00
ccostino
8b0af3fc32 Merge pull request #2906 from GSA/dependabot/pip/cffi-2.0.0
Bump cffi from 1.17.1 to 2.0.0
2025-09-11 12:46:25 -04:00
ccostino
d098d11b9e Merge pull request #2904 from GSA/dependabot/npm_and_yarn/rollup-4.50.1
Bump rollup from 4.50.0 to 4.50.1
2025-09-11 11:08:55 -04:00
dependabot[bot]
16b495003e Bump cffi from 1.17.1 to 2.0.0
Bumps [cffi](https://github.com/python-cffi/cffi) from 1.17.1 to 2.0.0.
- [Release notes](https://github.com/python-cffi/cffi/releases)
- [Commits](https://github.com/python-cffi/cffi/compare/v1.17.1...v2.0.0)

---
updated-dependencies:
- dependency-name: cffi
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 14:17:31 +00:00
ccostino
aed9f3e957 Merge pull request #2905 from GSA/dependabot/pip/pytest-playwright-0.7.1
Bump pytest-playwright from 0.7.0 to 0.7.1
2025-09-11 10:15:25 -04:00
ccostino
944b5eaa62 Merge pull request #2903 from GSA/dependabot/pip/moto-5.1.12
Bump moto from 5.1.11 to 5.1.12
2025-09-11 09:59:28 -04:00
ccostino
2c372b0464 Merge pull request #2902 from GSA/dependabot/npm_and_yarn/sass-embedded-1.92.1
Bump sass-embedded from 1.92.0 to 1.92.1
2025-09-11 09:36:50 -04:00
ccostino
524e8c87e8 Merge pull request #2901 from GSA/org_invites
fix org invites
2025-09-10 17:24:31 -04:00
Kenneth Kehl
4a14bf6f89 code review feedback 2025-09-10 10:29:50 -07:00
Kenneth Kehl
0fea345c8e ugh secrets 2025-09-09 12:57:25 -07:00
Kenneth Kehl
5d83cc43b2 cleanup 2025-09-09 12:43:15 -07:00
Kenneth Kehl
3d7aee19d2 fix org invite tests 2025-09-09 08:24:33 -07:00
Kenneth Kehl
a85a540abd try fixing tests 2025-09-09 07:28:45 -07:00
dependabot[bot]
d64766af32 Bump pytest-playwright from 0.7.0 to 0.7.1
Bumps [pytest-playwright](https://github.com/microsoft/playwright-pytest) from 0.7.0 to 0.7.1.
- [Release notes](https://github.com/microsoft/playwright-pytest/releases)
- [Commits](https://github.com/microsoft/playwright-pytest/compare/v0.7.0...v0.7.1)

---
updated-dependencies:
- dependency-name: pytest-playwright
  dependency-version: 0.7.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 23:08:41 +00:00
dependabot[bot]
c916b9e752 Bump rollup from 4.50.0 to 4.50.1
Bumps [rollup](https://github.com/rollup/rollup) from 4.50.0 to 4.50.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.50.0...v4.50.1)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.50.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 23:08:31 +00:00
dependabot[bot]
753cb0b132 Bump moto from 5.1.11 to 5.1.12
Bumps [moto](https://github.com/getmoto/moto) from 5.1.11 to 5.1.12.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.11...5.1.12)

---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 23:08:14 +00:00
dependabot[bot]
f5f6a95937 Bump sass-embedded from 1.92.0 to 1.92.1
Bumps [sass-embedded](https://github.com/sass/embedded-host-node) from 1.92.0 to 1.92.1.
- [Changelog](https://github.com/sass/embedded-host-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/embedded-host-node/compare/1.92.0...1.92.1)

---
updated-dependencies:
- dependency-name: sass-embedded
  dependency-version: 1.92.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 23:08:14 +00:00
Kenneth Kehl
180f372889 fix org invites 2025-09-08 12:44:45 -07:00
Kenneth Kehl
a5d2310253 fix org invites 2025-09-08 12:36:52 -07:00
Kenneth Kehl
de16e760ea fix org invites 2025-09-08 11:57:43 -07:00
Kenneth Kehl
31e72ea24b fix org invites 2025-09-08 08:23:04 -07:00
dependabot[bot]
f1b87f56ca Bump @babel/core from 7.28.3 to 7.28.4 (#2900)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.28.3 to 7.28.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 08:45:53 -04:00
ccostino
59a391f15b Merge pull request #2897 from GSA/dependabot/pip/newrelic-10.17.0
Bump newrelic from 10.16.0 to 10.17.0
2025-09-04 23:00:26 -04:00
dependabot[bot]
30e4e9c9dd Bump newrelic from 10.16.0 to 10.17.0
Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 10.16.0 to 10.17.0.
- [Release notes](https://github.com/newrelic/newrelic-python-agent/releases)
- [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v10.16.0...v10.17.0)

---
updated-dependencies:
- dependency-name: newrelic
  dependency-version: 10.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 02:26:47 +00:00
ccostino
2099f1f76b Merge pull request #2896 from GSA/dependabot/pip/markdown-3.9
Bump markdown from 3.8.2 to 3.9
2025-09-04 22:24:52 -04:00
dependabot[bot]
4181adb72d Bump markdown from 3.8.2 to 3.9
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: '3.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 02:05:29 +00:00
ccostino
7ecb69a39f Merge pull request #2898 from GSA/dependabot/pip/pytest-mock-3.15.0
Bump pytest-mock from 3.14.1 to 3.15.0
2025-09-04 22:03:29 -04:00
dependabot[bot]
d5df4316e4 Bump pytest-mock from 3.14.1 to 3.15.0
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.14.1 to 3.15.0.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.14.1...v3.15.0)

---
updated-dependencies:
- dependency-name: pytest-mock
  dependency-version: 3.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 01:40:23 +00:00
ccostino
ffe149937e Merge pull request #2895 from GSA/dependabot/pip/pytest-8.4.2
Bump pytest from 8.4.1 to 8.4.2
2025-09-04 21:38:27 -04:00
ccostino
afd11b4da9 Merge pull request #2894 from GSA/827-allow-users-to-move-between-steps-without-losing-data
added 'change csv button' on preview page
2025-09-04 21:21:57 -04:00
dependabot[bot]
4fa7f1fd25 Bump pytest from 8.4.1 to 8.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.1 to 8.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.1...8.4.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 23:05:40 +00:00
Beverly Nguyen
d724cc382e update href 2025-09-04 15:03:52 -07:00
Beverly Nguyen
5e8da220ec added 'change csv button on preview page' 2025-09-04 15:00:06 -07:00
ccostino
ece78d47a6 Merge pull request #2891 from GSA/dependabot/npm_and_yarn/jest-30.1.3
Bump jest from 30.1.1 to 30.1.3
2025-09-04 14:50:06 -04:00
dependabot[bot]
181ab3c4c9 Bump jest from 30.1.1 to 30.1.3
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 30.1.1 to 30.1.3.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.1.3/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 18:28:43 +00:00
ccostino
66313fe1a6 Merge pull request #2892 from GSA/dependabot/pip/virtualenv-20.34.0
Bump virtualenv from 20.32.0 to 20.34.0
2025-09-04 09:58:43 -04:00
dependabot[bot]
a6c4ccc280 Bump virtualenv from 20.32.0 to 20.34.0
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.32.0 to 20.34.0.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.32.0...20.34.0)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 07:26:07 +00:00
ccostino
497017769d Merge pull request #2890 from GSA/dependabot/npm_and_yarn/sass-embedded-1.92.0
Bump sass-embedded from 1.91.0 to 1.92.0
2025-09-03 16:59:24 -04:00
dependabot[bot]
6de72191bc Bump sass-embedded from 1.91.0 to 1.92.0
Bumps [sass-embedded](https://github.com/sass/embedded-host-node) from 1.91.0 to 1.92.0.
- [Changelog](https://github.com/sass/embedded-host-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/embedded-host-node/compare/1.91.0...1.92.0)

---
updated-dependencies:
- dependency-name: sass-embedded
  dependency-version: 1.92.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 19:00:27 +00:00
ccostino
b2a432c2d1 Merge pull request #2886 from GSA/dependabot/npm_and_yarn/rollup-4.50.0
Bump rollup from 4.49.0 to 4.50.0
2025-09-03 13:30:47 -04:00
dependabot[bot]
92a684e9a0 Bump rollup from 4.49.0 to 4.50.0
Bumps [rollup](https://github.com/rollup/rollup) from 4.49.0 to 4.50.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.50.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 15:58:09 +00:00
ccostino
a5141dae18 Merge pull request #2887 from GSA/dependabot/npm_and_yarn/jest-environment-jsdom-30.1.2
Bump jest-environment-jsdom from 30.1.1 to 30.1.2
2025-09-03 11:56:51 -04:00
ccostino
1d11ad21d8 Merge pull request #2875 from GSA/upgrade_python_again
upgrade python to 3.13.2
2025-09-03 11:34:47 -04:00
Kenneth Kehl
eeedbe869c merge from main 2025-09-03 08:12:49 -07:00
dependabot[bot]
810509711d Bump jest-environment-jsdom from 30.1.1 to 30.1.2
Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 30.1.1 to 30.1.2.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/HEAD/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest-environment-jsdom
  dependency-version: 30.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 15:10:57 +00:00
ccostino
5e95d67cbd Merge pull request #2885 from GSA/dependabot/npm_and_yarn/jest-30.1.2
Bump jest from 30.1.1 to 30.1.2
2025-09-03 11:09:31 -04:00
ccostino
7781a16ea4 Merge pull request #2888 from GSA/dependabot/pip/regex-2025.9.1
Bump regex from 2025.8.29 to 2025.9.1
2025-09-03 10:34:58 -04:00
dependabot[bot]
4f75d9d3b1 Bump regex from 2025.8.29 to 2025.9.1
Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2025.8.29 to 2025.9.1.
- [Changelog](https://github.com/mrabarnett/mrab-regex/blob/hg/changelog.txt)
- [Commits](https://github.com/mrabarnett/mrab-regex/compare/2025.8.29...2025.9.1)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 2025.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 12:36:43 +00:00
ccostino
77765e8d31 Merge pull request #2889 from GSA/dependabot/pip/cryptography-45.0.7
Bump cryptography from 45.0.6 to 45.0.7
2025-09-03 08:34:46 -04:00
dependabot[bot]
69bade1d84 Bump cryptography from 45.0.6 to 45.0.7
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.6 to 45.0.7.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.6...45.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 45.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 19:45:47 +00:00
dependabot[bot]
d94f324b82 Bump jest from 30.1.1 to 30.1.2
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 30.1.1 to 30.1.2.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/HEAD/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 16:58:04 +00:00
dependabot[bot]
a80c6b03d2 Bump regex from 2025.7.34 to 2025.8.29 (#2879)
Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2025.7.34 to 2025.8.29.
- [Changelog](https://github.com/mrabarnett/mrab-regex/blob/hg/changelog.txt)
- [Commits](https://github.com/mrabarnett/mrab-regex/compare/2025.7.34...2025.8.29)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 2025.8.29
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-30 22:38:46 +00:00
dependabot[bot]
8ec5178fb3 Bump gevent from 25.8.1 to 25.8.2 (#2881)
Bumps [gevent](https://github.com/gevent/gevent) from 25.8.1 to 25.8.2.
- [Release notes](https://github.com/gevent/gevent/releases)
- [Changelog](https://github.com/gevent/gevent/blob/master/docs/changelog_pre.rst)
- [Commits](https://github.com/gevent/gevent/compare/25.8.1...25.8.2)

---
updated-dependencies:
- dependency-name: gevent
  dependency-version: 25.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-30 22:30:13 +00:00
dependabot[bot]
83ae4d2b47 Bump phonenumbers from 9.0.12 to 9.0.13 (#2882)
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.12 to 9.0.13.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.12...v9.0.13)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-version: 9.0.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-30 22:21:42 +00:00
dependabot[bot]
282717d2f2 Bump coverage from 7.10.5 to 7.10.6 (#2880)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.10.5 to 7.10.6.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.10.5...7.10.6)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-30 18:13:01 -04:00
dependabot[bot]
1375ca7cfa Bump mistune from 3.1.3 to 3.1.4 (#2883)
Bumps [mistune](https://github.com/lepture/mistune) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/lepture/mistune/releases)
- [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst)
- [Commits](https://github.com/lepture/mistune/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: mistune
  dependency-version: 3.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-30 18:11:48 -04:00
dependabot[bot]
be3297aac8 Bump gevent from 25.5.1 to 25.8.1 (#2878)
Bumps [gevent](https://github.com/gevent/gevent) from 25.5.1 to 25.8.1.
- [Release notes](https://github.com/gevent/gevent/releases)
- [Changelog](https://github.com/gevent/gevent/blob/master/docs/changelog_pre.rst)
- [Commits](https://github.com/gevent/gevent/compare/25.5.1...25.8.1)

---
updated-dependencies:
- dependency-name: gevent
  dependency-version: 25.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 08:14:33 -04:00
Kenneth Kehl
29a9dc5026 upgrade python to 3.13.2 2025-08-28 08:26:46 -07:00
dependabot[bot]
f1b0a60899 Bump jest from 30.0.5 to 30.1.1 (#2873)
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 30.0.5 to 30.1.1.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.1.1/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-28 11:57:55 +00:00
dependabot[bot]
e906512d27 Bump rollup from 4.48.1 to 4.49.0 (#2871)
Bumps [rollup](https://github.com/rollup/rollup) from 4.48.1 to 4.49.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.48.1...v4.49.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.49.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-28 11:48:51 +00:00
dependabot[bot]
8b64c0dff0 Bump jest-each from 30.0.5 to 30.1.0 (#2872)
Bumps [jest-each](https://github.com/jestjs/jest/tree/HEAD/packages/jest-each) from 30.0.5 to 30.1.0.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.1.0/packages/jest-each)

---
updated-dependencies:
- dependency-name: jest-each
  dependency-version: 30.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-28 11:47:23 +00:00
dependabot[bot]
910af35b59 Bump jest-environment-jsdom from 30.0.5 to 30.1.1 (#2874)
Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 30.0.5 to 30.1.1.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.1.1/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest-environment-jsdom
  dependency-version: 30.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-28 07:40:10 -04:00
ccostino
8f37e7e618 Merge pull request #2869 from GSA/dependabot/npm_and_yarn/rollup-4.48.1
Bump rollup from 4.47.1 to 4.48.1
2025-08-27 09:10:23 -04:00
ccostino
8c5cce8190 Merge pull request #2868 from GSA/dependabot/pip/humanize-4.13.0
Bump humanize from 4.12.3 to 4.13.0
2025-08-27 08:34:23 -04:00
dependabot[bot]
258452dcbe Bump rollup from 4.47.1 to 4.48.1
Bumps [rollup](https://github.com/rollup/rollup) from 4.47.1 to 4.48.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.47.1...v4.48.1)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.48.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 18:10:56 +00:00
dependabot[bot]
d849a39a7e Bump humanize from 4.12.3 to 4.13.0
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.12.3 to 4.13.0.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.12.3...4.13.0)

---
updated-dependencies:
- dependency-name: humanize
  dependency-version: 4.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 17:07:00 +00:00
dependabot[bot]
76539e5bde Bump coverage from 7.10.4 to 7.10.5 (#2866)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.10.4 to 7.10.5.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.10.4...7.10.5)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:05:52 +00:00
dependabot[bot]
2e85677bb0 Bump cachetools from 6.1.0 to 6.2.0 (#2865)
Bumps [cachetools](https://github.com/tkem/cachetools) from 6.1.0 to 6.2.0.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v6.1.0...v6.2.0)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:03:25 +00:00
dependabot[bot]
15dd131096 Bump moto from 5.1.10 to 5.1.11 (#2867)
Bumps [moto](https://github.com/getmoto/moto) from 5.1.10 to 5.1.11.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.10...5.1.11)

---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 16:53:55 +00:00
dependabot[bot]
fe33285e9d Bump sass-embedded from 1.90.0 to 1.91.0 (#2863)
Bumps [sass-embedded](https://github.com/sass/embedded-host-node) from 1.90.0 to 1.91.0.
- [Changelog](https://github.com/sass/embedded-host-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/embedded-host-node/compare/1.90.0...1.91.0)

---
updated-dependencies:
- dependency-name: sass-embedded
  dependency-version: 1.91.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 12:10:56 -04:00
dependabot[bot]
4429fc69a6 Bump beautifulsoup4 from 4.13.4 to 4.13.5 (#2864)
Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.13.4 to 4.13.5.

---
updated-dependencies:
- dependency-name: beautifulsoup4
  dependency-version: 4.13.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 12:00:30 -04:00
ccostino
0c63c4620e Merge pull request #2861 from GSA/2859-csp-notices---daily-scans
daily scan fix CSP duplicate host warning
2025-08-26 11:20:44 -04:00
Beverly Nguyen
a797e44874 remove dups 2025-08-25 17:42:38 -07:00
Alex Janousek
9e2dc55500 Updated activity chart to include additional day (#2862) 2025-08-25 17:45:14 -04:00
dependabot[bot]
37a86ecb31 Bump rollup from 4.46.4 to 4.47.1 (#2860)
Bumps [rollup](https://github.com/rollup/rollup) from 4.46.4 to 4.47.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.46.4...v4.47.1)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.47.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 11:25:28 -04:00
Beverly Nguyen
28af41f72c prevent duplicate 2025-08-22 14:44:55 -07:00
Alex Janousek
b2f86e045f Sometimes test hang up and time out due to network errors (#2853)
* Fixed disable button, formated time to include time zone

* Fixed bandit

* Fix hanging test
2025-08-21 17:23:30 -04:00
dependabot[bot]
7cbd0602e6 Bump flask from 3.1.1 to 3.1.2 (#2847)
Bumps [flask](https://github.com/pallets/flask) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.1...3.1.2)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 10:34:55 -04:00
dependabot[bot]
63befd58f9 Bump playwright from 1.54.2 to 1.55.0 (#2851)
Bumps [playwright](https://github.com/microsoft/playwright) from 1.54.2 to 1.55.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.2...v1.55.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 10:33:21 -04:00
dependabot[bot]
6d9d147438 Bump rollup from 4.46.3 to 4.46.4 (#2852)
Bumps [rollup](https://github.com/rollup/rollup) from 4.46.3 to 4.46.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.46.3...v4.46.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.46.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 10:31:45 -04:00
Alex Janousek
c6da0448fa Fixed disable button, formated time to include time zone (#2850) 2025-08-21 07:05:41 -07:00
ccostino
54d14844fe Merge pull request #2849 from GSA/update-prod-qa-issue-template-2
Minor template folder permissions adjustments
2025-08-20 13:26:03 -04:00
Carlo Costino
735b657db1 Minor template folder permissions adjustments
This changeset adds a bit more clarity to the template folder permissions checklist.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-08-20 12:45:53 -04:00
ccostino
86e6ebe1dd Merge pull request #2848 from GSA/update-prod-qa-issue-template
Updated QA prod release checklist
2025-08-20 12:05:06 -04:00
Carlo Costino
78d1b42ede Updated QA prod release checklist
This changeset updates our QA prod release issue template to account for some recent changes to our reports and template folder handling, among other minor improvements.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-08-20 10:58:52 -04:00
ccostino
377595bf58 Merge pull request #2841 from GSA/2795-bug-when-a-new-template-folder-is-added-existing-users-are-automatically-given-permission-to-it
fix and update to template manage
2025-08-20 10:42:25 -04:00
Beverly Nguyen
6eac43b25e pytest 2025-08-19 14:17:21 -07:00
Beverly Nguyen
1740c57f08 labeling admin users and jest test 2025-08-19 14:16:19 -07:00
Beverly Nguyen
728038f161 prevent click event bubbling up 2025-08-19 13:16:52 -07:00
Beverly Nguyen
b1b63c7d5e seperated admin and regular users from ui 2025-08-19 13:00:43 -07:00
Beverly Nguyen
ca235f12fd providing creator of foldrs default permission 2025-08-19 12:43:58 -07:00
Alex Janousek
c22e029eb3 Fix for timezone conversion for reporting (#2845) 2025-08-19 13:03:06 -04:00
Alex Janousek
74c1648c8d Fix report download buttons showing as disabled (#2840)
* Fix for lazy loading while checking if file exists

* Fixed linting errors

* Fixed for static scan
2025-08-19 07:06:50 -07:00
dependabot[bot]
808aeabbe1 Bump requests from 2.32.4 to 2.32.5 (#2842)
Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.32.5.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.32.5)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 08:26:28 -04:00
dependabot[bot]
ce11464510 Bump coverage from 7.10.3 to 7.10.4 (#2843)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.10.3 to 7.10.4.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.10.3...7.10.4)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 08:24:53 -04:00
dependabot[bot]
b1727e5742 Bump rollup from 4.46.2 to 4.46.3 (#2844)
Bumps [rollup](https://github.com/rollup/rollup) from 4.46.2 to 4.46.3.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.46.2...v4.46.3)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.46.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 08:23:18 -04:00
Beverly Nguyen
00497b918d fix and update to template manage 2025-08-18 18:14:42 -07:00
Kenneth Kehl
3d784d4d79 Merge pull request #2838 from GSA/deactivate
fix deactivate
2025-08-18 07:27:45 -07:00
ccostino
cbdff04cca Merge pull request #2839 from GSA/dependabot/pip/phonenumbers-9.0.12
Bump phonenumbers from 9.0.11 to 9.0.12
2025-08-18 10:12:09 -04:00
dependabot[bot]
da8abac30c Bump phonenumbers from 9.0.11 to 9.0.12
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.11 to 9.0.12.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.11...v9.0.12)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-version: 9.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 23:10:03 +00:00
dependabot[bot]
5e4ae34bce Bump newrelic from 10.15.0 to 10.16.0 (#2835)
Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 10.15.0 to 10.16.0.
- [Release notes](https://github.com/newrelic/newrelic-python-agent/releases)
- [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v10.15.0...v10.16.0)

---
updated-dependencies:
- dependency-name: newrelic
  dependency-version: 10.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 19:21:11 +00:00
dependabot[bot]
b5f181a271 Bump pyproj from 3.7.1 to 3.7.2 (#2834)
Bumps [pyproj](https://github.com/pyproj4/pyproj) from 3.7.1 to 3.7.2.
- [Release notes](https://github.com/pyproj4/pyproj/releases)
- [Changelog](https://github.com/pyproj4/pyproj/blob/main/docs/history.rst)
- [Commits](https://github.com/pyproj4/pyproj/compare/3.7.1...3.7.2)

---
updated-dependencies:
- dependency-name: pyproj
  dependency-version: 3.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 19:11:57 +00:00
dependabot[bot]
0cb1060659 Bump @babel/preset-env from 7.28.0 to 7.28.3 (#2836)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.28.0 to 7.28.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 19:11:00 +00:00
dependabot[bot]
fd90394dcd Bump @babel/core from 7.28.0 to 7.28.3 (#2837)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.28.0 to 7.28.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 15:03:31 -04:00
Alex Janousek
8d33f28b76 Refactored reports to use pregenerated docs instead (#2831)
* Refactored reports to use pregenerated docs instead

* Fixed e2e test

* Fixed anothr bug

* Cleanup

* Fixed timezone conversion

* Updated ref files

* Updated reference files, refreshed ui/ux for report generation. Buttons toggle on and off based on if report exists

* Fixed linting errors, removed pytz

* Fixed test failure

* e2e test fix

* Speeding up unit tests

* Removed python time library that was causing performance issues with unit tests

* Updated poetry lock

* Unit test improvements

* Made change that ken reccomended
2025-08-15 15:02:54 -04:00
Kenneth Kehl
20db7929b5 fix deactivate 2025-08-15 09:02:58 -07:00
Kenneth Kehl
a48269d210 fix deactivate 2025-08-15 08:31:36 -07:00
ccostino
748c35d2df Merge pull request #2832 from GSA/setup_profile_fix
fix login.gov error
2025-08-14 14:36:43 -04:00
Kenneth Kehl
a59b820995 fix login.gov error 2025-08-14 10:46:45 -07:00
ccostino
e202ab0eb7 Merge pull request #2830 from GSA/dependabot/pip/virtualenv-20.34.0
Bump virtualenv from 20.33.1 to 20.34.0
2025-08-14 09:28:39 -04:00
dependabot[bot]
bf9d61e537 Bump virtualenv from 20.33.1 to 20.34.0
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.33.1 to 20.34.0.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.33.1...20.34.0)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 23:10:15 +00:00
dependabot[bot]
a12ed166d5 Bump pre-commit from 4.2.0 to 4.3.0 (#2824)
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 17:07:50 +00:00
dependabot[bot]
d65f3feaaf Bump coverage from 7.10.2 to 7.10.3 (#2826)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.10.2 to 7.10.3.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.10.2...7.10.3)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 16:54:46 +00:00
dependabot[bot]
d678ea857c Bump charset-normalizer from 3.4.2 to 3.4.3 (#2827)
Bumps [charset-normalizer](https://github.com/jawah/charset_normalizer) from 3.4.2 to 3.4.3.
- [Release notes](https://github.com/jawah/charset_normalizer/releases)
- [Changelog](https://github.com/jawah/charset_normalizer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jawah/charset_normalizer/compare/3.4.2...3.4.3)

---
updated-dependencies:
- dependency-name: charset-normalizer
  dependency-version: 3.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 16:54:09 +00:00
dependabot[bot]
1ea5797807 Bump moto from 5.1.9 to 5.1.10 (#2825)
Bumps [moto](https://github.com/getmoto/moto) from 5.1.9 to 5.1.10.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.9...5.1.10)

---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 12:50:39 -04:00
dependabot[bot]
95428f32c7 Bump freezegun from 1.5.4 to 1.5.5 (#2828)
Bumps [freezegun](https://github.com/spulec/freezegun) from 1.5.4 to 1.5.5.
- [Release notes](https://github.com/spulec/freezegun/releases)
- [Changelog](https://github.com/spulec/freezegun/blob/master/CHANGELOG)
- [Commits](https://github.com/spulec/freezegun/compare/1.5.4...1.5.5)

---
updated-dependencies:
- dependency-name: freezegun
  dependency-version: 1.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 12:45:54 -04:00
ccostino
1a6b061657 Merge pull request #2823 from GSA/dependabot/pip/phonenumbers-9.0.11
Bump phonenumbers from 9.0.10 to 9.0.11
2025-08-08 10:54:30 -04:00
dependabot[bot]
b3cb1c4405 Bump phonenumbers from 9.0.10 to 9.0.11
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.10 to 9.0.11.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/r9.0.10...v9.0.11)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-version: 9.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-08 14:38:18 +00:00
ccostino
012c47eb80 Merge pull request #2821 from GSA/dependabot/pip/cryptography-45.0.6
Bump cryptography from 45.0.5 to 45.0.6
2025-08-08 10:36:26 -04:00
dependabot[bot]
30c715bfd9 Bump cryptography from 45.0.5 to 45.0.6
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.5 to 45.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.5...45.0.6)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 45.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-08 14:10:01 +00:00
ccostino
9207bd6876 Merge pull request #2822 from GSA/dependabot/pip/redis-6.4.0
Bump redis from 6.3.0 to 6.4.0
2025-08-08 10:08:08 -04:00
Beverly Nguyen
bc3657d566 Merge pull request #2815 from GSA/2813-add-quick-filters
add quick filters for easy data consumption
2025-08-07 21:33:27 -07:00
Beverly Nguyen
dbfeb32eb2 clean up 2025-08-07 18:49:09 -07:00
Beverly Nguyen
2853a49f0c clean up 2025-08-07 18:43:04 -07:00
Beverly Nguyen
0a25d75146 fix filtering to use processing_started time 2025-08-07 18:31:53 -07:00
dependabot[bot]
f5bfce516b Bump redis from 6.3.0 to 6.4.0
Bumps [redis](https://github.com/redis/redis-py) from 6.3.0 to 6.4.0.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v6.3.0...v6.4.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-07 23:28:27 +00:00
Beverly Nguyen
e8fb7117c0 Merge branch 'main' into 2813-add-quick-filters 2025-08-07 14:50:01 -07:00
Beverly Nguyen
24f2cb3566 Merge pull request #2812 from GSA/2802-making-the-data-more-scannable-through-better-formatting-and-status-indicators
cleaning up css and add better visual for pagination
2025-08-07 14:34:13 -07:00
dependabot[bot]
0a11838a73 Bump certifi from 2025.7.14 to 2025.8.3 (#2805)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.7.14 to 2025.8.3.
- [Commits](https://github.com/certifi/python-certifi/compare/2025.07.14...2025.08.03)

---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2025.8.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 21:00:19 +00:00
Beverly Nguyen
7afcebfc87 npm test 2025-08-07 13:00:32 -07:00
Alex Janousek
5c00ee1840 Fix/main build 2 (#2819)
* Removed double slash

* Fixing race condition with testing

* Created cleaner solution

* Keeping flash alive longer so tests finish in main build
2025-08-07 12:52:56 -07:00
Beverly Nguyen
bd9948ed7b Backstop 2025-08-07 12:35:13 -07:00
Beverly Nguyen
b6b8223eb2 removed sort 2025-08-07 12:30:25 -07:00
Beverly Nguyen
c779f38258 Refactored the activity table to use meaningful data attributes instead of CSS classes for JavaScript functionality. 2025-08-07 12:29:15 -07:00
Beverly Nguyen
4f82c69323 flake 8 2025-08-07 11:51:25 -07:00
Beverly Nguyen
e4929c835e fixed pytest 2025-08-07 11:46:01 -07:00
Beverly Nguyen
9739fe8d24 backstop 2025-08-07 11:14:43 -07:00
Beverly Nguyen
736e7c34f9 backstop reference and table scroll 2025-08-07 11:04:42 -07:00
Beverly Nguyen
43aa02d6f2 no text wrap 2025-08-07 10:55:40 -07:00
Beverly Nguyen
c4c1cc6046 font c hanges 2025-08-07 10:15:11 -07:00
Beverly Nguyen
d774ab89d1 fix syntax 2025-08-07 10:06:46 -07:00
Beverly Nguyen
28cd113f37 Merge branch 'main' into 2813-add-quick-filters 2025-08-07 09:58:08 -07:00
Beverly Nguyen
894e8c7d1b adding status column 2025-08-07 09:57:44 -07:00
Alex Janousek
843699061d Removed double slash (#2817) 2025-08-07 11:55:26 -04:00
Beverly Nguyen
7fc6be815a Merge branch 'main' into 2802-making-the-data-more-scannable-through-better-formatting-and-status-indicators 2025-08-06 13:41:33 -07:00
Beverly Nguyen
562a57bd85 formatting files 2025-08-06 13:40:11 -07:00
Beverly Nguyen
511b0b8b9e add in test and fix isort 2025-08-06 13:36:28 -07:00
Beverly Nguyen
463be2a46b add quick filters for easy data consumption without scrolling through many pages 2025-08-06 13:24:28 -07:00
Beverly Nguyen
95421c6d1e cleaning up css and add better visual for pagination 2025-08-05 22:46:49 -07:00
dependabot[bot]
12cd3d711e Bump sass-embedded from 1.89.2 to 1.90.0 (#2808)
Bumps [sass-embedded](https://github.com/sass/embedded-host-node) from 1.89.2 to 1.90.0.
- [Changelog](https://github.com/sass/embedded-host-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/embedded-host-node/compare/1.89.2...1.90.0)

---
updated-dependencies:
- dependency-name: sass-embedded
  dependency-version: 1.90.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 20:32:04 -04:00
dependabot[bot]
26a06e0f4d Bump redis from 6.2.0 to 6.3.0 (#2809)
Bumps [redis](https://github.com/redis/redis-py) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v6.2.0...v6.3.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 20:31:00 -04:00
dependabot[bot]
068a7b9a3d Bump virtualenv from 20.33.0 to 20.33.1 (#2810)
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.33.0 to 20.33.1.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.33.0...20.33.1)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.33.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 20:29:54 -04:00
dependabot[bot]
129f75e045 Bump virtualenv from 20.32.0 to 20.33.0 (#2806)
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.32.0 to 20.33.0.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.32.0...20.33.0)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 12:54:54 -04:00
dependabot[bot]
8feaff29ae Bump coverage from 7.10.1 to 7.10.2 (#2807)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.10.1 to 7.10.2.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.10.1...7.10.2)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 12:54:40 -04:00
Alex Janousek
bca68e06c5 Merge pull request #2804 from GSA/dependabot/npm_and_yarn/playwright-1.54.2
Bump playwright from 1.54.1 to 1.54.2
2025-08-04 12:53:54 -04:00
Alex Janousek
69eb8e09b2 Merge pull request #2803 from GSA/dependabot/pip/regex-2025.7.34
Bump regex from 2025.7.33 to 2025.7.34
2025-08-04 12:53:42 -04:00
dependabot[bot]
281c1e7e3a Bump playwright from 1.54.1 to 1.54.2
Bumps [playwright](https://github.com/microsoft/playwright) from 1.54.1 to 1.54.2.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.1...v1.54.2)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.54.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 23:07:58 +00:00
dependabot[bot]
8ecf72b74f Bump regex from 2025.7.33 to 2025.7.34
Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2025.7.33 to 2025.7.34.
- [Changelog](https://github.com/mrabarnett/mrab-regex/blob/hg/changelog.txt)
- [Commits](https://github.com/mrabarnett/mrab-regex/compare/2025.7.33...2025.7.34)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 2025.7.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-31 23:55:35 +00:00
Alex Janousek
3ecd9853ab Merge pull request #2800 from GSA/2589-may-19-zap-scan-fixes
Updated based on scan results
2025-07-31 19:03:42 -04:00
alexjanousekGSA
aa18d1447e Reran reference tests and merged in main to fix conflicts 2025-07-31 17:07:17 -04:00
alexjanousekGSA
1203546a97 Merge branch 'main' into 2589-may-19-zap-scan-fixes
Resolved conflicts by:
- Taking all backstop images from main branch
- Taking .ds.baseline from main (with updated line numbers)
- Keeping template literal security fixes in listEntry.js while incorporating main's styling changes
- Taking main's config.py updates (LOGO_CDN_DOMAIN and formatting)
2025-07-31 16:08:12 -04:00
Beverly Nguyen
cb84fba9f6 Merge pull request #2794 from GSA/2085-conditional-within-the-my-activity-page-needs-to-be-fixed
Fix empty table display in My activity view when user has no jobs
2025-07-31 12:15:28 -07:00
Alex Janousek
1b52da73b7 Merge pull request #2774 from GSA/govuk-migration
More govuk conversions
2025-07-31 15:11:24 -04:00
alexjanousekGSA
1168e221df Removed hogan from tests that were leftover 2025-07-31 13:52:35 -04:00
alexjanousekGSA
51c9cf9e74 Merge branch 'main' into govuk-migration 2025-07-31 13:40:38 -04:00
alexjanousekGSA
c04d8969f5 Fixed linting and build errors 2025-07-31 13:38:50 -04:00
ccostino
307e52a7d6 Merge pull request #2801 from GSA/fix-gunicorn-config
Remove logging from gunicorn config file
2025-07-31 13:07:37 -04:00
Carlo Costino
bf171ebaab Remova unneeded hilite import
Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-31 12:58:57 -04:00
Carlo Costino
5e4e763119 Remove logging from gunicorn config file
This changeset removes the logging statements in the gunicorn config file to see if that addresses the issue we are seeing.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-31 12:55:25 -04:00
alexjanousekGSA
611ada696f Security scan updates 2025-07-31 12:47:40 -04:00
alexjanousekGSA
55340dbc4b Updated based on scan results 2025-07-31 11:39:58 -04:00
alexjanousekGSA
29d050a0b8 Fixed unit tests 2025-07-31 09:54:08 -04:00
alexjanousekGSA
6942a2843d Tweaked a few more issues 2025-07-31 09:24:44 -04:00
alexjanousekGSA
ab7b262c01 Fixed more unit tests 2025-07-31 08:14:58 -04:00
Alex Janousek
6a3d9c40b4 Merge pull request #2798 from GSA/dependabot/pip/freezegun-1.5.4
Bump freezegun from 1.5.3 to 1.5.4
2025-07-31 08:13:11 -04:00
dependabot[bot]
43b68f9337 Bump freezegun from 1.5.3 to 1.5.4
Bumps [freezegun](https://github.com/spulec/freezegun) from 1.5.3 to 1.5.4.
- [Release notes](https://github.com/spulec/freezegun/releases)
- [Changelog](https://github.com/spulec/freezegun/blob/master/CHANGELOG)
- [Commits](https://github.com/spulec/freezegun/compare/1.5.3...1.5.4)

---
updated-dependencies:
- dependency-name: freezegun
  dependency-version: 1.5.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-31 12:07:19 +00:00
Alex Janousek
690c164c21 Merge pull request #2784 from GSA/dependabot/pip/coverage-7.10.1
Bump coverage from 7.10.0 to 7.10.1
2025-07-31 08:04:55 -04:00
Alex Janousek
59f6cccbed Merge pull request #2791 from GSA/dependabot/npm_and_yarn/rollup-4.46.2
Bump rollup from 4.45.1 to 4.46.2
2025-07-31 08:04:33 -04:00
Alex Janousek
2d7271e55b Merge pull request #2792 from GSA/dependabot/npm_and_yarn/morphdom-2.7.7
Bump morphdom from 2.7.5 to 2.7.7
2025-07-31 08:04:25 -04:00
Alex Janousek
b8ec2e03f0 Merge pull request #2785 from GSA/dependabot/pip/moto-5.1.9
Bump moto from 5.1.8 to 5.1.9
2025-07-31 08:04:12 -04:00
Alex Janousek
e1b530f16c Merge pull request #2799 from GSA/dependabot/pip/regex-2025.7.33
Bump regex from 2024.11.6 to 2025.7.33
2025-07-31 08:03:56 -04:00
alexjanousekGSA
b1e5b8d110 Fixed merge conflict 2025-07-31 07:53:37 -04:00
alexjanousekGSA
31a9e39846 Formatting and linting changes 2025-07-31 07:49:12 -04:00
dependabot[bot]
e3901f80f8 Bump regex from 2024.11.6 to 2025.7.33
Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2024.11.6 to 2025.7.33.
- [Changelog](https://github.com/mrabarnett/mrab-regex/blob/hg/changelog.txt)
- [Commits](https://github.com/mrabarnett/mrab-regex/compare/2024.11.6...2025.7.33)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 2025.7.33
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 23:44:53 +00:00
alexjanousekGSA
7d5db34d16 Increased unit test 2025-07-30 18:46:19 -04:00
Beverly Nguyen
4f489c1445 jest test fix 2025-07-30 15:06:44 -07:00
alexjanousekGSA
be93596075 More cleanup and changes for stable pr 2025-07-30 15:46:53 -04:00
alexjanousekGSA
9e97a797b9 Migrate GOV.UK components to USWDS
- Convert previous/next navigation to USWDS pagination component
  - Update radio buttons and checkboxes to use USWDS classes
  - Migrate guest list inputs with proper USWDS styling
  - Replace GOV.UK button patterns with USWDS button variants
  - Update form field spacing to use USWDS utilities
  - Scope fieldset legend styles to specific form contexts
  - Update visual regression test references for new components
2025-07-30 15:24:21 -04:00
Kenneth Kehl
a81742c5c5 Merge pull request #2797 from GSA/report_performance
change gunicorn timeout to 240 seconds
2025-07-30 12:16:12 -07:00
Kenneth Kehl
1a5c56b621 ugh 2025-07-30 11:31:22 -07:00
Kenneth Kehl
4c258bb39b ugh 2025-07-30 11:25:36 -07:00
Kenneth Kehl
d35c7bbd5f temporarily weaken axe checks 2025-07-30 11:15:21 -07:00
Kenneth Kehl
e82e138771 change gunicorn timeout to 240 seconds 2025-07-30 10:51:54 -07:00
ccostino
e3910e008c Merge pull request #2782 from GSA/2257-there-shouldnt-be-a-next-in-pagination-if-theres-less-than-50-items-in-a-table
fixing pagination and download links along with tests
2025-07-30 13:20:03 -04:00
ccostino
902e3e85f7 Merge pull request #2796 from GSA/downgrade-python-again
Downgrade Python to 3.12.9 again
2025-07-30 12:50:37 -04:00
Carlo Costino
f3df2f9c33 Downgrade Python to 3.12.9 again
This changeset puts us back to Python 3.12.9 since we are still wrestling with certificate validation errors in Python 3.13 and no easy way to test things without breaking our deployment flow currently.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-30 12:24:16 -04:00
Beverly Nguyen
177f7e836e Merge branch 'main' into 2085-conditional-within-the-my-activity-page-needs-to-be-fixed 2025-07-29 22:27:03 -07:00
Beverly Nguyen
260a58986b Fix empty table display in My activity view when user has no jobs 2025-07-29 22:23:59 -07:00
dependabot[bot]
7e789b1b55 Bump morphdom from 2.7.5 to 2.7.7
Bumps [morphdom](https://github.com/patrick-steele-idem/morphdom) from 2.7.5 to 2.7.7.
- [Changelog](https://github.com/patrick-steele-idem/morphdom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/patrick-steele-idem/morphdom/compare/v2.7.5...v2.7.7)

---
updated-dependencies:
- dependency-name: morphdom
  dependency-version: 2.7.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 23:24:01 +00:00
dependabot[bot]
a8dd322b82 Bump rollup from 4.45.1 to 4.46.2
Bumps [rollup](https://github.com/rollup/rollup) from 4.45.1 to 4.46.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.45.1...v4.46.2)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.46.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 23:23:53 +00:00
ccostino
35361fa784 Merge pull request #2790 from GSA/modify-notifications-client
Attempt to debug SSL cert validation error
2025-07-29 18:12:34 -04:00
Carlo Costino
3db62a1921 Fix broken test
Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-29 17:55:04 -04:00
Carlo Costino
bf9d17d660 Fixed formatting issue with missing line
Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-29 17:39:10 -04:00
Carlo Costino
f85efba737 Attempt to debug SSL cert validation error
This changeset introduces a few changes:

* Removes UK Notify Logo URL and config
* Removes UK Notify API URL and replaces with Notify.gov
* Improves the error handler for HTTPErrors to provide actual exception and stack trace information

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-29 17:33:13 -04:00
ccostino
c1b071adea Merge pull request #2788 from GSA/undo-recent-changes
Undo recent changes related to Python upgrade
2025-07-29 16:40:47 -04:00
Carlo Costino
f69e3a5a00 Undo recent changes related to Python upgrade
This changeset undoes the recent changes we tried after the Python 3.13 update as they had no bearing on the SSL cert validation errors.  Back to the drawing board!

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-29 16:32:52 -04:00
ccostino
c2b7575e99 Merge pull request #2787 from GSA/python_3_13
disable gevent
2025-07-29 14:35:08 -04:00
Kenneth Kehl
9d072e44df add comment 2025-07-29 11:21:24 -07:00
Kenneth Kehl
8593a9b1ba disable gevent 2025-07-29 10:31:06 -07:00
Beverly Nguyen
e5885f674b Fixed pagination logic to prevent infinite next button 2025-07-29 10:25:43 -07:00
Beverly Nguyen
d65be41da5 fix test get_immediate_jobs for download availability check 2025-07-29 10:17:35 -07:00
Beverly Nguyen
eac2733a33 flake8 2025-07-29 09:57:59 -07:00
ccostino
18a6730a2c Merge pull request #2786 from GSA/python_3_13
instantiate newrelic in gunicorn_entry.py
2025-07-29 12:00:22 -04:00
Kenneth Kehl
c8a559f0e5 instantiate newrelic in gunicorn_entry.py 2025-07-29 08:15:43 -07:00
ccostino
d7568eda42 Merge pull request #2781 from GSA/python_3_13
upgrade to python 3.13.2
2025-07-29 10:04:36 -04:00
ccostino
0860d41042 Fix version number to match
Document the correct version.
2025-07-29 09:54:39 -04:00
ccostino
5a37482510 Fix version number to match
Document the correct version.
2025-07-29 09:54:30 -04:00
ccostino
cd14024627 Fix version number to match
Document the correct version.
2025-07-29 09:54:20 -04:00
dependabot[bot]
d16bfb39ba Bump moto from 5.1.8 to 5.1.9
Bumps [moto](https://github.com/getmoto/moto) from 5.1.8 to 5.1.9.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.8...5.1.9)

---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 02:31:12 +00:00
dependabot[bot]
cb94d26b73 Bump coverage from 7.10.0 to 7.10.1
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.10.0 to 7.10.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.10.0...7.10.1)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 02:12:25 +00:00
Kenneth Kehl
814d08af2b upgrade to python 3.13.2 2025-07-28 11:56:55 -07:00
ccostino
c4842e9e06 Merge pull request #2780 from GSA/try_gevent
try to force gevent monkey patching earlier
2025-07-28 14:25:27 -04:00
Kenneth Kehl
99585ae9f0 try to force gevent monkey patching earlier 2025-07-28 11:17:00 -07:00
ccostino
411854b481 Merge pull request #2779 from GSA/try_gevent
add gevent monkeypatching
2025-07-28 13:13:35 -04:00
Kenneth Kehl
6f663c83eb add gevent monkeypatching 2025-07-28 10:01:44 -07:00
ccostino
a6f52fa009 Merge pull request #2778 from GSA/try_gevent
switch eventlet to gevent
2025-07-28 12:34:35 -04:00
Kenneth Kehl
ee47610d2a switch eventlet to gevent 2025-07-28 09:21:46 -07:00
Alex Janousek
05381a5ba9 Merge pull request #2776 from GSA/dependabot/pip/newrelic-10.15.0
Bump newrelic from 10.14.0 to 10.15.0
2025-07-25 12:46:16 -04:00
Alex Janousek
32c35eb5f5 Merge pull request #2777 from GSA/dependabot/pip/coverage-7.10.0
Bump coverage from 7.9.2 to 7.10.0
2025-07-25 12:45:55 -04:00
ccostino
105cab2314 Merge pull request #2775 from GSA/dependabot/pip/numpy-2.3.2
Bump numpy from 2.3.1 to 2.3.2
2025-07-25 12:14:03 -04:00
dependabot[bot]
8d6ebe762a Bump coverage from 7.9.2 to 7.10.0
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.9.2 to 7.10.0.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.9.2...7.10.0)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-24 23:19:00 +00:00
dependabot[bot]
d8a98dd5d5 Bump newrelic from 10.14.0 to 10.15.0
Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 10.14.0 to 10.15.0.
- [Release notes](https://github.com/newrelic/newrelic-python-agent/releases)
- [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v10.14.0...v10.15.0)

---
updated-dependencies:
- dependency-name: newrelic
  dependency-version: 10.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-24 23:17:34 +00:00
dependabot[bot]
17ca0cee62 Bump numpy from 2.3.1 to 2.3.2
Bumps [numpy](https://github.com/numpy/numpy) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-24 23:17:09 +00:00
ccostino
0f1d6c698c Merge pull request #2772 from GSA/update-readme-terraform
Updated README Terraform setup instructions
2025-07-24 16:26:15 -04:00
ccostino
033895cf42 Merge pull request #2773 from GSA/system_certs
revert truststore
2025-07-24 16:07:55 -04:00
Kenneth Kehl
b5b0d23b7e revert truststore 2025-07-24 10:49:30 -07:00
Kenneth Kehl
fa2e5becea Merge pull request #2765 from GSA/system_certs
try truststore
2025-07-24 07:07:05 -07:00
Kenneth Kehl
64cd520b8e merge from main 2025-07-23 15:01:03 -07:00
Alex Janousek
9f3855270d Merge pull request #2768 from GSA/dependabot/npm_and_yarn/jest-environment-jsdom-30.0.5
Bump jest-environment-jsdom from 30.0.4 to 30.0.5
2025-07-23 17:51:06 -04:00
dependabot[bot]
206e76a417 Bump jest-environment-jsdom from 30.0.4 to 30.0.5
Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 30.0.4 to 30.0.5.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest-environment-jsdom
  dependency-version: 30.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 21:29:49 +00:00
Alex Janousek
265e564b49 Merge pull request #2769 from GSA/dependabot/pip/eventlet-0.40.2
Bump eventlet from 0.40.1 to 0.40.2
2025-07-23 17:28:29 -04:00
Alex Janousek
b15decc8fb Merge pull request #2766 from GSA/dependabot/npm_and_yarn/jest-30.0.5
Bump jest from 30.0.4 to 30.0.5
2025-07-23 17:28:17 -04:00
Carlo Costino
99194c08d0 Updated README Terraform setup instructio
This changeset updates the setup instructions for Terraform to use a newer tool to manage Terraform installations, and to update the Terraform version used within the project.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2025-07-23 13:48:03 -04:00
dependabot[bot]
6b23ea46e7 Bump eventlet from 0.40.1 to 0.40.2
Bumps [eventlet](https://github.com/eventlet/eventlet) from 0.40.1 to 0.40.2.
- [Changelog](https://github.com/eventlet/eventlet/blob/master/NEWS)
- [Commits](https://github.com/eventlet/eventlet/compare/0.40.1...0.40.2)

---
updated-dependencies:
- dependency-name: eventlet
  dependency-version: 0.40.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 23:58:30 +00:00
dependabot[bot]
32f8a31685 Bump jest from 30.0.4 to 30.0.5
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 30.0.4 to 30.0.5.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 23:07:41 +00:00
Kenneth Kehl
ed06cf5695 fix 2025-07-22 11:35:37 -07:00
Kenneth Kehl
6034dcdb12 try truststore 2025-07-22 11:01:20 -07:00
139 changed files with 5025 additions and 3696 deletions

View File

@@ -161,7 +161,7 @@
"filename": "app/config.py",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false,
"line_number": 123,
"line_number": 122,
"is_secret": false
}
],
@@ -527,7 +527,7 @@
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473",
"is_verified": false,
"line_number": 115,
"line_number": 118,
"is_secret": false
},
{
@@ -535,7 +535,7 @@
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 185,
"line_number": 188,
"is_secret": false
},
{
@@ -543,7 +543,7 @@
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8",
"is_verified": false,
"line_number": 256,
"line_number": 259,
"is_secret": false
}
],
@@ -629,10 +629,10 @@
"filename": "tests/conftest.py",
"hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea",
"is_verified": false,
"line_number": 3267,
"line_number": 3287,
"is_secret": false
}
]
},
"generated_at": "2025-07-22T17:07:31Z"
"generated_at": "2025-09-09T19:57:05Z"
}

View File

@@ -1,12 +1,12 @@
name: "QA Checklist"
description: "Use this template to manually QA for prod push"
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
title: "QA checklist for production release: [insert planned release date here]"
body:
- type: checkboxes
id: Logging_in
id: sign_in
attributes:
label: Log In
label: Sign In
options:
- label: notify-staging.app.cloud.gov resolves
- label: Clicking sign in takes you to Login.gov
@@ -25,9 +25,8 @@ body:
- label: Formatting looks clean and in place
- label: Message dashboard populates and shows “pending” until call is made to AWS for delivery receipt
- type: checkboxes
id: send__copied_template
id: send_copied_template
attributes:
label: Send a message from copied template
options:
@@ -37,7 +36,7 @@ body:
- type: checkboxes
id: send__existing_template
id: send_existing_template
attributes:
label: Send a message from existing template
options:
@@ -45,13 +44,25 @@ body:
- label: All else above persists as true (formatting, send, dashboard, etc)
- type: checkboxes
id: download_report
id: template_folder_permissions
attributes:
label: Download the csv delivery report
label: Checking template folder permissions
options:
- label: Clicking on the csv download directly after a message send downloads a report that contains the data we expect
- label: The batch report appears on the main dashboard batch table with accurate information
- label: Clicking on the csv download link from the main dashboard downloads a report that contains the data we expect
- label: Only the template creator and platform admins should be able to see a new template folder
- label: Adding a non platform admin user to a folder should grant them access to see and copy the templates
- label: Removing a non platform admin user to a folder should remove their access to see and copy the templates
- label: Platform admins should not be able to be added or removed from template folders, since they have full access regardless
- type: checkboxes
id: download_reports
attributes:
label: Download the CSV delivery reports
options:
- label: Clicking on the CSV download directly after a message send downloads a report that contains the data we expect
- label: Clicking on the CSV download links from the main dashboard downloads a report that contains the data we expect
- label: The daily generated batch reports appear at the bottom of the main dashboard batch table with accurate information
- label: The daily generated batch report buttons are highlighted appropriately based on which reports are available (1, 3, 5, and 7 day)
- label: Downloading the generated batch reports that are available work properly and contain the data that we expect
- type: checkboxes
id: invite_member

View File

@@ -9,10 +9,10 @@ runs:
sudo apt-get update \
&& sudo apt-get install -y --no-install-recommends \
libcurl4-openssl-dev
- name: Set up Python 3.12.9
- name: Set up Python 3.13.2
uses: actions/setup-python@v4
with:
python-version: "3.12.9"
python-version: "3.13.2"
- name: Install poetry
shell: bash
run: pip install poetry==2.1.3

View File

@@ -100,9 +100,9 @@ jobs:
# tail -f admin-server.log &
# above make e2e-test
run: |
make run-flask > admin-server.log 2>&1 &
make wait-for-flask
tail -f admin-server.log &
make e2e-test
@@ -116,7 +116,7 @@ jobs:
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_URI: http://localhost:6012/
NOTIFY_E2E_TEST_URI: http://localhost:6012
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
validate-new-relic-config:

View File

@@ -15,7 +15,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
## DEVELOPMENT
.PHONY: bootstrap
bootstrap: ## Set up everything to run the app
bootstrap: ## Set up everything to run the application
make generate-version-file
poetry sync --no-root
poetry run playwright install --with-deps
@@ -44,6 +44,19 @@ watch-frontend: ## Build frontend and watch for changes
run-flask: ## Run flask
poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0
.PHONY: wait-for-flask
wait-for-flask:
@echo "Waiting for Flask to start..."
@timeout 30 bash -c 'until curl -sf http://localhost:6012 > /dev/null 2>&1; do sleep 1; done'
@echo "Flask is ready!"
.PHONY: run-flask-and-wait
run-flask-and-wait:
@make run-flask &
@echo "Waiting for Flask to start..."
@timeout 30 bash -c 'until curl -sf http://localhost:6012 > /dev/null 2>&1; do sleep 1; done'
@echo "Flask is ready!"
.PHONY: run-flask-bare
run-flask-bare: ## Run flask without invoking poetry so we can override ENV variables in .env
flask run -p 6012 --host=0.0.0.0
@@ -67,6 +80,9 @@ generate-version-file: ## Generates the app version file
.PHONY: test
test: py-lint py-test js-test ## Run tests
.PHONY: test-fast
test-fast: py-lint py-test-fast js-test ## Run tests quickly in parallel (testing locally first)
.PHONY: py-lint
py-lint: ## Run python linting scanners and black
poetry self add poetry-dotenv-plugin
@@ -98,6 +114,11 @@ py-test: ## Run python unit tests
poetry run coverage report --fail-under=93
poetry run coverage html -d .coverage_cache
.PHONY: py-test-fast
py-test-fast: export NEW_RELIC_ENVIRONMENT=test
py-test-fast: ## Run python unit tests in parallel (testing locally first)
poetry run pytest --maxfail=10 --ignore=tests/end_to_end tests/ -n auto
.PHONY: dead-code
dead-code: ## 60% is our aspirational goal, but currently breaks the build
poetry run vulture ./app ./notifications_utils --min-confidence=100

View File

@@ -130,7 +130,7 @@ configuration after installation to get working out of the box:
- [jq](https://stedolan.github.io/jq/) - for working with JSON in the command
line
- [git](https://git-scm.com/) - for version control management
- [tfenv](https://github.com/tfutils/tfenv) - for managing
- [tenv](https://github.com/tofuutils/tenv) - for managing
[Terraform](https://www.terraform.io/) installations
- [cf-cli@8](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) - for
working with a Cloud Foundry platform (e.g., cloud.gov)
@@ -147,15 +147,18 @@ brew install jq git tfenv cloudfoundry/tap/cf-cli@8 vim wget
#### [API Step] Terraform Installation
As a part of the installation above, you just installed `tfenv` to manage
As a part of the installation above, you just installed `tenv` to manage
Terraform installations. This is great, but you still need to install Terraform
itself, which can be done with this command:
```sh
tfenv install latest:^1.4.0
tenv
```
_NOTE: This project currently uses the latest `1.4.x release of Terraform._
This will open a menu for you; choose Terraform, then choose the latest stable
version.
_NOTE: This project currently uses the latest `1.12.x release of Terraform._
#### [API Step] Python Installation
@@ -186,12 +189,12 @@ session to make the changes take effect.
Now we're ready to install the Python version we need with `pyenv`, like so:
```sh
pyenv install 3.12
pyenv install 3.13
```
This will install the latest version of Python 3.12.
This will install the latest version of Python 3.13.
_NOTE: This project currently runs on Python 3.12.x._
_NOTE: This project currently runs on Python 3.13.x._
#### [API Step] Python Dependency Installation
@@ -243,12 +246,12 @@ git clone git@github.com:GSA/notifications-admin.git
Now go into the project directory (`notifications-admin` by default), create a
virtual environment, and set the local Python version to point to the virtual
environment (assumes version Python `3.12.9` is what is installed on your
environment (assumes version Python `3.13.2` is what is installed on your
machine):
```sh
cd notifications-admin
pyenv virtualenv 3.12.9 notify-admin
pyenv virtualenv 3.13.2 notify-admin
pyenv local notify-admin
```
@@ -281,10 +284,10 @@ If you're upgrading an existing project to a newer version of Python, you can
follow these steps to get yourself up-to-date.
First, use `pyenv` to install the newer version of Python you'd like to use;
we'll use `3.12` in our example here since we recently upgraded to this version:
we'll use `3.13` in our example here since we recently upgraded to this version:
```sh
pyenv install 3.12
pyenv install 3.13
```
Next, delete the virtual environment you previously had set up. If you followed
@@ -299,7 +302,7 @@ environment with the newer version of Python you just installed:
```sh
cd notifications-admin
pyenv virtualenv 3.12.9 notify-admin
pyenv virtualenv 3.13.2 notify-admin
pyenv local notify-admin
```

View File

@@ -2,6 +2,7 @@ import os
import pathlib
import re
import secrets
import sys
from functools import partial
from time import monotonic
from urllib.parse import unquote, urlparse, urlunparse
@@ -101,11 +102,17 @@ from app.notify_client.organizations_api_client import organizations_client
from app.notify_client.performance_dashboard_api_client import (
performance_dashboard_api_client,
)
from app.notify_client.platform_stats_api_client import platform_stats_api_client
from app.notify_client.platform_stats_api_client import (
platform_stats_api_client,
)
from app.notify_client.service_api_client import service_api_client
from app.notify_client.status_api_client import status_api_client
from app.notify_client.template_folder_api_client import template_folder_api_client
from app.notify_client.template_statistics_api_client import template_statistics_client
from app.notify_client.template_folder_api_client import (
template_folder_api_client,
)
from app.notify_client.template_statistics_api_client import (
template_statistics_client,
)
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
@@ -141,7 +148,6 @@ navigation = {
def _csp(config):
asset_domain = config["ASSET_DOMAIN"]
logo_domain = config["LOGO_CDN_DOMAIN"]
api_public_url = config["API_PUBLIC_URL"]
api_public_ws_url = config["API_PUBLIC_WS_URL"]
@@ -157,7 +163,6 @@ def _csp(config):
"script-src": [
"'self'",
asset_domain,
"'unsafe-eval'",
"https://js-agent.newrelic.com",
"https://gov-bam.nr-data.net",
"https://www.googletagmanager.com",
@@ -165,25 +170,22 @@ def _csp(config):
"https://dap.digitalgov.gov",
"https://cdn.socket.io",
],
"connect-src": [
"connect-src": list(dict.fromkeys([
"'self'",
"https://gov-bam.nr-data.net",
"https://www.google-analytics.com",
f"{api_public_url}",
f"{api_public_ws_url}",
],
])),
"style-src": ["'self'", asset_domain],
"img-src": ["'self'", asset_domain, logo_domain],
"img-src": ["'self'", asset_domain],
}
def create_app(application):
@application.after_request
def add_csp_header(response):
existing_csp = response.headers.get("Content-Security-Policy", "")
response.headers["Content-Security-Policy"] = (
existing_csp + "; form-action 'self';"
)
def add_security_headers(response):
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
return response
@application.context_processor
@@ -271,14 +273,11 @@ def create_app(application):
content_security_policy=_csp(application.config),
content_security_policy_nonce_in=["style-src", "script-src"],
permissions_policy={
"accelerometer": "()",
"ambient-light-sensor": "()",
"autoplay": "()",
"battery": "()",
"accelerometer": '(self "https://www.youtube-nocookie.com")',
"autoplay": '(self "https://www.youtube-nocookie.com")',
"camera": "()",
"document-domain": "()",
"geolocation": "()",
"gyroscope": "()",
"gyroscope": '(self "https://www.youtube-nocookie.com")',
"local-fonts": "()",
"magnetometer": "()",
"microphone": "()",
@@ -539,26 +538,28 @@ def register_errorhandlers(application): # noqa (C901 too complex)
@application.errorhandler(HTTPError)
def render_http_error(error):
error_url = error.response.url if error.response else "unknown URL"
application.logger.warning(
"API {} failed with status {} message {}".format(
error.response.url if error.response else "unknown",
error.status_code,
error.message,
)
f"API {error_url} failed with status {error.status_code} message {error.message}",
exc_info=sys.exc_info(),
stack_info=True,
)
error_code = error.status_code
if error_code not in [401, 404, 403, 410]:
# probably a 500 or 503.
# it might be a 400, which we should handle as if it's an internal server error. If the API might
# legitimately return a 400, we should handle that within the view or the client that calls it.
application.logger.exception(
"API {} failed with status {} message {}".format(
error.response.url if error.response else "unknown",
error.status_code,
error.message,
)
f"API {error_url} failed with status {error.status_code} message {error.message}",
exc_info=sys.exc_info(),
stack_info=True,
)
error_code = 500
return _error_response(error_code)
@application.errorhandler(400)

View File

@@ -5,6 +5,9 @@
const tableContainer = document.getElementById('activityContainer');
const currentUserName = tableContainer.getAttribute('data-currentUserName');
const currentServiceId = tableContainer.getAttribute('data-currentServiceId');
let pollInterval;
let isPolling = false;
const POLL_INTERVAL_MS = 25000;
const COLORS = {
delivered: '#0076d6',
failed: '#fa9441',
@@ -153,8 +156,6 @@
.on('mouseout', function() {
tooltip.style('display', 'none');
})
.transition()
.duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => {
const calculatedHeight = y(d[0]) - y(d[1]);
@@ -209,28 +210,35 @@
table.append(tbody);
};
const fetchData = function(type) {
const fetchData = async function(type) {
if (isPolling) {
return;
}
if (document.hidden) {
return;
}
var ctx = document.getElementById('weeklyChart');
if (!ctx) {
return;
}
isPolling = true;
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
var url = type === 'service'
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
: `/services/${currentServiceId}/daily-stats-by-user.json?timezone=${encodeURIComponent(userTimezone)}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
const data = await response.json();
labels = [];
deliveredData = [];
failedData = [];
@@ -277,10 +285,41 @@
}
return data;
})
.catch(error => console.error('Error fetching daily stats:', error));
};
setInterval(() => fetchData(currentType), 25000);
} catch (error) {
console.error('Error fetching daily stats:', error);
} finally {
isPolling = false;
}
};
function startPolling() {
fetchData(currentType);
pollInterval = setInterval(() => {
fetchData(currentType);
}, POLL_INTERVAL_MS);
}
function stopPolling() {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling();
} else {
stopPolling();
startPolling();
}
});
if (typeof jest === 'undefined') {
startPolling();
}
const handleDropdownChange = function(event) {
const selectedValue = event.target.value;
currentType = selectedValue;
@@ -295,37 +334,50 @@
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
const tableHeading = document.querySelector('#tableActivity h2');
const senderColumns = document.querySelectorAll('.sender-column');
const senderElements = [
document.querySelector('[data-column="sender"]'),
...document.querySelectorAll('[data-sender]')
];
const allRows = document.querySelectorAll('#activity-table tbody tr');
const caption = document.querySelector('#activity-table caption');
const table = document.getElementById('activity-table');
const caption = table.querySelector('caption');
if (selectedValue === 'individual') {
tableHeading.textContent = 'My activity';
caption.textContent = `Table showing the sent jobs for ${currentUserName}`;
senderColumns.forEach(col => {
col.style.display = 'none';
senderElements.forEach(el => {
if (el) el.style.display = 'none';
});
allRows.forEach(row => row.style.display = 'none');
const userRows = Array.from(allRows).filter(row => {
const senderCell = row.querySelector('.sender-column');
const rowSender = senderCell ? senderCell.textContent.trim() : '';
const senderCell = row.querySelector('[data-sender]');
const rowSender = senderCell ? senderCell.dataset.sender : '';
return rowSender === currentUserName;
});
userRows.slice(0, 5).forEach(row => {
row.style.display = '';
});
if (userRows.length > 0) {
userRows.slice(0, 5).forEach(row => {
row.style.display = '';
});
} else {
const emptyMessageRow = Array.from(allRows).find(row => {
return row.querySelector('.table-empty-message');
});
if (emptyMessageRow) {
emptyMessageRow.style.display = '';
}
}
} else {
tableHeading.textContent = 'Service activity';
caption.textContent = `Table showing the sent jobs for service`;
senderColumns.forEach(col => {
col.style.display = '';
senderElements.forEach(el => {
if (el) el.style.display = '';
});
allRows.forEach((row, index) => {

View File

@@ -118,7 +118,8 @@
CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() {
const headingLevel = this.$component.data('heading-level') || '2';
this.$heading = $(`<h${headingLevel} class="heading-small">${this.legendText}</h${headingLevel}>`);
this.$heading = $(`<h${headingLevel} class="heading-small"></h${headingLevel}>`);
this.$heading.text(this.legendText);
this.$fieldset.before(this.$heading);
this.$fieldset.find('legend').addClass('usa-sr-only');
@@ -138,6 +139,7 @@
};
CollapsibleCheckboxes.prototype.toggleAll = function(e) {
e.preventDefault();
e.stopPropagation();
const allChecked = this.$checkboxes.filter(':checked').length === this.$checkboxes.length;
if (allChecked) {

View File

@@ -17,6 +17,7 @@
this.minEntries = 2;
this.listItemName = this.$wrapper.data('listItemName');
this.getSharedAttributes();
this.getOriginalClasses();
this.getValues();
this.maxEntries = this.entries.length;
@@ -25,31 +26,33 @@
this.bindEvents();
};
ListEntry.optionalAttributes = ['aria-describedby'];
ListEntry.prototype.entryTemplate = Hogan.compile(
'<div class="list-entry">' +
'<label for="{{{id}}}" class="govuk-input--numbered__label">' +
'<span class="usa-sr-only">{{listItemName}} number </span>{{number}}.' +
'</label>' +
'<input' +
' name="{{name}}"' +
' id="{{id}}"' +
' {{#value}}value="{{value}}{{/value}}"' +
' {{{sharedAttributes}}}' +
'/>' +
'{{#button}}' +
'<button type="button" class="usa-button input-list__button--remove">' +
'Remove<span class="usa-sr-only"> {{listItemName}} number {{number}}</span>' +
'</button>' +
'{{/button}}' +
'</div>'
);
ListEntry.prototype.addButtonTemplate = Hogan.compile(
'<button type="button" class="usa-button input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
);
ListEntry.prototype.renderEntry = function(data) {
return `
<div class="list-entry">
<label for="${data.id}" class="usa-label">
<span class="usa-sr-only">${data.listItemName} number </span>${data.number}.
</label>
<input
class="usa-input ${data.classes || ''}"
name="${data.name}"
id="${data.id}"
${data.value ? `value="${data.value}"` : ''}
${data.sharedAttributes}
/>
${data.button ? `
<button type="button" class="usa-button usa-button--unstyled input-list__button--remove">
Remove<span class="usa-sr-only"> ${data.listItemName} number ${data.number}</span>
</button>
` : ''}
</div>
`;
};
ListEntry.prototype.renderAddButton = function(data) {
return `<button type="button" class="usa-button usa-button--outline input-list__button--add margin-top-4">Add another ${data.listItemName} (${data.entriesLeft} remaining)</button>`;
};
ListEntry.prototype.getSharedAttributes = function () {
var $inputs = this.$wrapper.find('input'),
attributeTemplate = Hogan.compile(' {{name}}="{{value}}"'),
generatedAttributes = ['id', 'name', 'value'],
generatedAttributes = ['id', 'name', 'value', 'class'],
attributes = [],
attrIdx,
elmAttributes,
@@ -68,7 +71,7 @@
while (attrIdx--) {
// prevent duplicates
if ($.inArray(elmAttrs[attrIdx].name, existingAttributes) === -1) {
attrStr += attributeTemplate.render({ 'name': elmAttrs[attrIdx].name, 'value': elmAttrs[attrIdx].value });
attrStr += ` ${elmAttrs[attrIdx].name}="${elmAttrs[attrIdx].value}"`;
existingAttributes.push(elmAttrs[attrIdx].name);
}
}
@@ -94,6 +97,20 @@
this.sharedAttributes = (attributes.length) ? getAttributesHTML(attributes) : '';
};
ListEntry.prototype.getOriginalClasses = function () {
var $firstInput = this.$wrapper.find('input').first();
if ($firstInput.length) {
var classList = $firstInput.attr('class');
if (classList) {
// Preserve any additional classes from the original input
this.additionalClasses = classList;
} else {
this.additionalClasses = '';
}
} else {
this.additionalClasses = '';
}
};
ListEntry.prototype.getValues = function () {
this.entries = [];
this.$wrapper.find('input').each(function (idx, elm) {
@@ -182,16 +199,17 @@
'name' : this.getId(entryNumber),
'value' : entry,
'listItemName' : this.listItemName,
'sharedAttributes': this.sharedAttributes
'sharedAttributes': this.sharedAttributes,
'classes': this.additionalClasses
};
if (entryNumber > 1) {
dataObj.button = true;
}
this.$wrapper.append(this.entryTemplate.render(dataObj));
this.$wrapper.append(this.renderEntry(dataObj));
}.bind(this));
if (this.entries.length < this.maxEntries) {
this.$wrapper.append(this.addButtonTemplate.render({
this.$wrapper.append(this.renderAddButton({
'listItemName' : this.listItemName,
'entriesLeft' : (this.maxEntries - this.entries.length)
}));

View File

@@ -7,9 +7,6 @@ $(() => GOVUK.modules.start());
$(() => $('.error-message, .usa-error-message').eq(0).parent('label').next('input').trigger('focus'));
$(() => $('.govuk-header__container').on('click', function() {
$(this).css('border-color', '#005ea5');
}));
// Applies our expanded focus style to the siblings of links when that link is wrapped in a heading.
//

View File

@@ -14,7 +14,6 @@ import Button from 'govuk-frontend/components/button/button';
import Radios from 'govuk-frontend/components/radios/radios';
// Modules from 3rd party vendors
import morphdom from 'morphdom';
/**
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
@@ -67,13 +66,8 @@ var Frontend = {
"initAll": initAll
}
var vendor = {
"morphdom": morphdom
}
// The exported object will be assigned to window.GOVUK in our production code
// (bundled into an IIFE by RollupJS)
export {
Frontend,
vendor
Frontend
}

View File

@@ -3,65 +3,70 @@
"use strict";
var Modules = global.GOVUK.Modules;
var Hogan = global.Hogan;
// Object holding all the states for the component's HTML
let states = {
'initial': Hogan.compile(`
{{#showNowAsDefault}}
// Template functions for rendering component states
let renderStates = {
'initial': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
` : ''}
<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>
${data.categories.map(category =>
`<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='${category}' />`
).join('')}
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
{{#categories}}
<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='{{.}}' />
{{/categories}}
</div>
`),
'choose': Hogan.compile(`
{{#showNowAsDefault}}
`;
},
'choose': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
` : ''}
<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>
${data.choices.map(choice => `
<div class="usa-radio js-option">
<input class="usa-radio__input" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
</div>
{{/showNowAsDefault}}
<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>
`;
},
'chosen': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
{{/choices}}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
</div>
`),
'chosen': Hogan.compile(`
{{#showNowAsDefault}}
` : ''}
<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>
${data.choices.map(choice => `
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
</div>
{{/showNowAsDefault}}
<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}}" />
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
</div>
{{/choices}}
</div>
<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>
`)
<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>
`;
}
};
let shiftFocus = function(elementToFocus, component) {
@@ -80,7 +85,7 @@
let $component = $(component);
let render = (state, data) => {
$component.html(states[state].render(data));
$component.html(renderStates[state](data));
};
// store array of all options in component
let choices = $('label', $component).toArray().map(function(element) {

View File

@@ -0,0 +1,13 @@
document.querySelectorAll('.usa-button-group a, .usa-pagination a').forEach(function(button) {
button.addEventListener('click', function() {
sessionStorage.setItem('scrollPosition', window.pageYOffset);
});
});
document.addEventListener('DOMContentLoaded', function() {
var scrollPosition = sessionStorage.getItem('scrollPosition');
if (scrollPosition !== null) {
window.scrollTo(0, parseInt(scrollPosition));
sessionStorage.removeItem('scrollPosition');
}
});

View File

@@ -1,11 +1,3 @@
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
document.addEventListener('DOMContentLoaded', function () {
const isJobPage = window.location.pathname.includes('/jobs/');
if (!isJobPage) return;
@@ -15,69 +7,115 @@ document.addEventListener('DOMContentLoaded', function () {
const featureEnabled = jobEl?.dataset?.feature === 'true';
const apiHost = jobEl?.dataset?.host;
if (!jobId) return;
if (!jobId || !featureEnabled) return;
if (featureEnabled) {
const socket = io(apiHost);
const DEFAULT_INTERVAL_MS = 10000;
const MIN_INTERVAL_MS = 1000;
const MAX_INTERVAL_MS = 30000;
socket.on('connect_error', (err) => {
console.error('Socket connect_error:', err);
});
let pollInterval;
let currentInterval = DEFAULT_INTERVAL_MS;
let isPolling = false;
socket.on('error', (err) => {
console.error('Socket error:', err);
});
socket.on('connect', () => {
socket.emit('join', { room: `job-${jobId}` });
});
window.addEventListener('beforeunload', () => {
socket.emit('leave', { room: `job-${jobId}` });
});
const debouncedUpdate = debounce((data) => {
updateAllJobSections();
}, 1000);
socket.on('job_updated', (data) => {
if (data.job_id !== jobId) return;
debouncedUpdate(data);
});
function calculateBackoff(responseTime) {
return Math.min(
MAX_INTERVAL_MS,
Math.max(
MIN_INTERVAL_MS,
Math.floor((250 * Math.sqrt(responseTime)) - 1000)
)
);
}
function updateAllJobSections() {
async function updateAllJobSections() {
if (isPolling || document.hidden) {
return;
}
isPolling = true;
const startTime = Date.now();
const resourceEl = document.querySelector('[data-socket-update="status"]');
const url = resourceEl?.dataset?.resource;
if (!url) {
console.warn('No resource URL found for job updates');
isPolling = false;
return;
}
fetch(url)
.then((res) => res.json())
.then(({ status, counts, notifications }) => {
const sections = {
status: document.querySelector('[data-socket-update="status"]'),
counts: document.querySelector('[data-socket-update="counts"]'),
notifications: document.querySelector(
'[data-socket-update="notifications"]'
),
};
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (status && sections.status) {
sections.status.innerHTML = status;
}
if (counts && sections.counts) {
sections.counts.innerHTML = counts;
}
if (notifications && sections.notifications) {
sections.notifications.innerHTML = notifications;
}
})
.catch((err) => {
console.error('Error fetching job update partials:', err);
const sections = {
status: document.querySelector('[data-socket-update="status"]'),
counts: document.querySelector('[data-socket-update="counts"]'),
notifications: document.querySelector('[data-socket-update="notifications"]'),
};
if (data.status && sections.status) {
sections.status.innerHTML = data.status;
}
if (data.counts && sections.counts) {
sections.counts.innerHTML = data.counts;
}
if (data.notifications && sections.notifications) {
sections.notifications.innerHTML = data.notifications;
}
const responseTime = Date.now() - startTime;
currentInterval = calculateBackoff(responseTime);
if (data.stop === 1 || data.finished === true) {
stopPolling();
}
} catch (error) {
console.error('Error fetching job updates:', {
error: error.message,
url: url,
jobId: jobId,
timestamp: new Date().toISOString()
});
currentInterval = Math.min(currentInterval * 2, MAX_INTERVAL_MS);
} finally {
isPolling = false;
}
}
function startPolling() {
updateAllJobSections();
function scheduleNext() {
if (pollInterval) clearTimeout(pollInterval);
pollInterval = setTimeout(() => {
updateAllJobSections();
scheduleNext();
}, currentInterval);
}
scheduleNext();
}
function stopPolling() {
if (pollInterval) {
clearTimeout(pollInterval);
pollInterval = null;
}
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling();
} else {
startPolling();
}
});
window.addEventListener('beforeunload', stopPolling);
startPolling();
});

View File

@@ -1,148 +0,0 @@
(function(global) {
"use strict";
var queues = {};
var morphdom = global.GOVUK.vendor.morphdom;
var defaultInterval = 2000;
var interval = 0;
var calculateBackoff = responseTime => parseInt(Math.max(
(250 * Math.sqrt(responseTime)) - 1000,
1000
));
// Methods to ensure the DOM fragment is clean of classes added by JS before diffing
// and that they are replaced afterwards.
//
// Added to allow the use of JS, in main.js, to apply styles which in future could be
// achieved with the :has pseudo-class. If :has is available in our supported browsers,
// this can be removed in favour of a CSS-only solution.
var ClassesPersister = function ($contents) {
this._$contents = $contents;
this._classNames = [];
this._classesTo$ElsMap = {};
};
ClassesPersister.prototype.addClassName = function (className) {
if (this._classNames.indexOf(className) === -1) {
this._classNames.push(className);
}
};
ClassesPersister.prototype.remove = function () {
// Store references to any elements with class names to persist
this._classNames.forEach(className => {
var $elsWithClassName = $('.' + className, this._$contents).removeClass(className);
if ($elsWithClassName.length > 0) {
this._classesTo$ElsMap[className] = $elsWithClassName;
}
});
};
ClassesPersister.prototype.replace = function () {
var replaceClasses = (idx, el) => {
// Avoid updating elements that are no longer present.
// elements removed will still exist in memory but won't be attached to the DOM any more
if (global.document.body.contains(el)) {
$(el).addClass(className);
}
};
var className;
for (className in this._classesTo$ElsMap) {
this._classesTo$ElsMap[className].each(replaceClasses);
}
// remove references to elements
this._classesTo$ElsMap = {};
};
var getRenderer = ($contents, key, classesPersister) => response => {
classesPersister.remove();
morphdom(
$contents.get(0),
$(response[key]).get(0)
);
classesPersister.replace();
};
var getQueue = resource => (
queues[resource] = queues[resource] || []
);
var flushQueue = function(queue, response) {
while(queue.length) queue.shift()(response);
};
var clearQueue = queue => (queue.length = 0);
var poll = function(renderer, resource, queue, form) {
let startTime = Date.now();
if (document.visibilityState !== "hidden" && queue.push(renderer) === 1) {
$.ajax(
resource,
{
'method': form ? 'post' : 'get',
'data': form ? $('#' + form).serialize() : {}
}
).done(
response => {
flushQueue(queue, response);
if (response.stop === 1) {
poll = function(){};
}
interval = calculateBackoff(Date.now() - startTime);
}
).fail(
() => poll = function(){}
);
}
setTimeout(
() => poll.apply(window, arguments), interval
);
};
global.GOVUK.Modules.UpdateContent = function() {
this.start = component => {
var $component = $(component);
var $contents = $component.children().eq(0);
var key = $component.data('key');
var resource = $component.data('resource');
var form = $component.data('form');
var classesPersister = new ClassesPersister($contents);
// Replace component with contents.
// The renderer does this anyway when diffing against the first response
$component.replaceWith($contents);
// Store any classes that should persist through updates
//
// Added to allow the use of JS, in main.js, to apply styles which in future could be
// achieved with the :has pseudo-class. If :has is available in our supported browsers,
// this can be removed in favour of a CSS-only solution.
if ($contents.data('classesToPersist') !== undefined) {
$contents.data('classesToPersist')
.split(' ')
.forEach(className => classesPersister.addClassName(className));
}
setTimeout(
() => poll(
getRenderer($contents, key, classesPersister),
resource,
getQueue(resource),
form
),
defaultInterval
);
};
};
global.GOVUK.Modules.UpdateContent.calculateBackoff = calculateBackoff;
})(window);

View File

@@ -492,44 +492,12 @@ td.table-empty-message {
.job-table {
width: 100%;
border-collapse: collapse;
td.file-name {
width: 25%;
overflow-wrap: anywhere;
}
td.jobid {
width: 5%;
}
td.template {
width: 25%;
}
td.time-sent {
width: 15%;
}
td.sender {
width: 20%;
overflow-wrap: break-word;
}
td.count-of-recipients {
width: 5%;
}
td.report {
width: 2%;
text-align: center;
}
td.delivered {
width: 2%;
text-align: center;
}
td.failed {
width: 2%;
text-align: center;
}
td.report img {
padding-top: 5px;
}
th {
padding: 0.5rem 1rem;
}
td {
padding: 0.5rem 1rem;
}
}
@media (max-width: 768px) {
@@ -1092,3 +1060,14 @@ nav.nav {
.selection-content .usa-form-group--nested li.usa-checkbox {
margin-top: 0;
}
// Radio button and checkbox legends
.usa-radio legend.usa-legend,
.usa-checkbox legend.usa-legend,
.api-key-radios legend.usa-legend,
.login-auth-radios legend.usa-legend,
.auth-type-radios legend.usa-legend,
.notification-type-radios legend.usa-legend,
.organizations-radios legend.usa-legend {
font-weight: 700;
}

View File

@@ -16,12 +16,11 @@ class Config(object):
API_PUBLIC_WS_URL = getenv("API_PUBLIC_WS_URL", "localhost")
ADMIN_BASE_URL = getenv("ADMIN_BASE_URL", "http://localhost:6012")
HEADER_COLOUR = (
"#81878b" # mix of dark-grey and mid-grey
)
HEADER_COLOUR = "#81878b" # mix of dark-grey and mid-grey
LOGO_CDN_DOMAIN = (
"static-logos.notifications.service.gov.uk" # TODO use our own CDN
)
ASSETS_DEBUG = False
# Credentials

View File

@@ -11,14 +11,13 @@ import ago
import dateutil
import humanize
import markdown
import pytz
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.enums import AuthType, NotificationStatus
from app.utils.csv import get_user_preferred_timezone
from app.utils.csv import get_user_preferred_timezone, get_user_preferred_timezone_obj
from app.utils.time import parse_naive_dt
from notifications_utils.field import Field
from notifications_utils.formatters import make_quotes_smart
@@ -119,7 +118,7 @@ def format_datetime_table(date):
def format_time_12h(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
)
@@ -140,7 +139,7 @@ def format_datetime_numeric(date):
def format_date_numeric(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
)
@@ -149,14 +148,14 @@ def format_date_numeric(date):
def format_time_24h(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%H:%M")
def get_human_day(time, date_prefix=""):
# Add 1 minute to transform 00:00 into midnight today instead of midnight tomorrow
time = parse_naive_dt(time)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
time = time.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
date = (time - timedelta(minutes=1)).date()
now = datetime.now(preferred_tz)
@@ -181,7 +180,7 @@ def get_human_day(time, date_prefix=""):
def format_date(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return (
date.replace(tzinfo=timezone.utc)
.astimezone(preferred_tz)
@@ -196,7 +195,7 @@ def format_date_normal(date):
def format_date_short(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return _format_datetime_short(
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
)
@@ -216,7 +215,7 @@ def format_datetime_human(date, date_prefix=""):
def format_day_of_week(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%A")
@@ -422,7 +421,7 @@ def redact_mobile_number(mobile_number, spacing=""):
return "".join(mobile_number_list)
def get_time_left(created_at, service_data_retention_days=7):
def get_time_left(created_at, service_data_retention_days=8):
return ago.human(
(datetime.now(timezone.utc))
- (

View File

@@ -3,7 +3,6 @@ from datetime import datetime, timedelta
from itertools import chain
from numbers import Number
import pytz
from flask import render_template, request
from flask_login import current_user
from flask_wtf import FlaskForm as Form
@@ -62,7 +61,7 @@ from app.main.validators import (
)
from app.models.organization import Organization
from app.utils import merge_jsonlike
from app.utils.csv import get_user_preferred_timezone
from app.utils.csv import get_user_preferred_timezone, get_user_preferred_timezone_obj
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
@@ -70,7 +69,7 @@ from notifications_utils.recipients import InvalidPhoneError, validate_phone_num
def get_time_value_and_label(future_time):
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return (
future_time.astimezone(preferred_tz).isoformat(),
"{} at {} {}".format(
@@ -90,7 +89,7 @@ def get_human_time(time):
def get_human_day(time, prefix_today_with="T"):
# Add 1 hour to get midnight today instead of midnight tomorrow
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
time = (time - timedelta(hours=1)).strftime("%A")
if time == datetime.now(preferred_tz).strftime("%A"):
return "{}oday".format(prefix_today_with)
@@ -101,12 +100,12 @@ def get_human_day(time, prefix_today_with="T"):
def get_furthest_possible_scheduled_time():
# We want local time to find date boundaries
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return (datetime.now(preferred_tz) + timedelta(days=4)).replace(hour=0)
def get_next_hours_until(until):
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
now = datetime.now(preferred_tz)
hours = int((until - now).total_seconds() / (60 * 60))
return [
@@ -116,7 +115,7 @@ def get_next_hours_until(until):
def get_next_days_until(until):
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
now = datetime.now(preferred_tz)
days = int((until - now).total_seconds() / (60 * 60 * 24))
return [
@@ -1727,10 +1726,18 @@ class TemplateFolderForm(StripWhitespaceForm):
def __init__(self, all_service_users=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if all_service_users is not None:
self.users_with_permission.all_service_users = all_service_users
self.users_with_permission.choices = [
(item.id, item.name) for item in all_service_users
regular_users = [
user for user in all_service_users if not user.platform_admin
]
platform_admins = [
user for user in all_service_users if user.platform_admin
]
self.users_with_permission.all_service_users = regular_users
self.users_with_permission.choices = [
(item.id, item.name) for item in regular_users
]
self.platform_admins = platform_admins
users_with_permission = USWDSCollapsibleCheckboxesField(
"Team members who can see this folder", field_label="team member"

View File

@@ -1,3 +1,4 @@
import gevent
from flask import abort, render_template, request, url_for
from app import current_service, job_api_client
@@ -13,42 +14,128 @@ from app.utils.pagination import (
from app.utils.user import user_has_permissions
def get_download_availability(service_id):
"""
Check if there are jobs available for each download time period.
"""
jobs_1_day = job_api_client.get_page_of_jobs(service_id, page=1, limit_days=1)
jobs_3_days = job_api_client.get_page_of_jobs(service_id, page=1, limit_days=3)
jobs_5_days = job_api_client.get_page_of_jobs(service_id, page=1, limit_days=5)
jobs_7_days = job_api_client.get_immediate_jobs(service_id)
def get_report_info(service_id, report_name, s3_config):
try:
from app.s3_client import check_s3_file_exists, get_s3_object
from app.s3_client.s3_csv_client import NEW_FILE_LOCATION_STRUCTURE
has_1_day_data = len(generate_job_dict(jobs_1_day)) > 0
has_3_day_data = len(generate_job_dict(jobs_3_days)) > 0
has_5_day_data = len(generate_job_dict(jobs_5_days)) > 0
has_7_day_data = len(jobs_7_days) > 0
key = NEW_FILE_LOCATION_STRUCTURE.format(service_id, report_name)
obj = get_s3_object(
s3_config["bucket"],
key,
s3_config["access_key_id"],
s3_config["secret_access_key"],
s3_config["region"],
)
exists = check_s3_file_exists(obj)
if exists:
# check_s3_file_exists already called obj.load(), so metadata should be populated
size_bytes = obj.content_length
# Only show as available if file has any content (not empty)
if size_bytes > 0:
if size_bytes < 1024:
size_str = f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
size_str = f"{size_bytes / 1024:.1f} KB"
else:
size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
return {"available": True, "size": size_str}
except Exception: # nosec B110
pass
return {"available": False, "size": None}
def get_download_availability(service_id):
from flask import current_app
# Get S3 config before spawning greenlets
s3_config = {
"bucket": current_app.config["CSV_UPLOAD_BUCKET"]["bucket"],
"access_key_id": current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"],
"secret_access_key": current_app.config["CSV_UPLOAD_BUCKET"][
"secret_access_key"
],
"region": current_app.config["CSV_UPLOAD_BUCKET"]["region"],
}
report_names = ["1-day-report", "3-day-report", "5-day-report", "7-day-report"]
greenlets = [
gevent.spawn(get_report_info, service_id, name, s3_config)
for name in report_names
]
gevent.joinall(greenlets)
results = [g.value for g in greenlets]
return {
"has_1_day_data": has_1_day_data,
"has_3_day_data": has_3_day_data,
"has_5_day_data": has_5_day_data,
"has_7_day_data": has_7_day_data,
"has_any_download_data": has_1_day_data
or has_3_day_data
or has_5_day_data
or has_7_day_data,
"report_1_day": (
results[0] if results[0] else {"available": False, "size": None}
),
"report_3_day": (
results[1] if results[1] else {"available": False, "size": None}
),
"report_5_day": (
results[2] if results[2] else {"available": False, "size": None}
),
"report_7_day": (
results[3] if results[3] else {"available": False, "size": None}
),
}
def get_download_links(message_type):
time_periods = ["one_day", "three_day", "five_day", "seven_day"]
links = {}
for period in time_periods:
links[f"download_link_{period}"] = url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days=period,
)
return links
def get_filtered_jobs(service_id, page):
filter_type = request.args.get("filter")
limit_days = None
if filter_type == "24hours":
limit_days = 1
elif filter_type == "3days":
limit_days = 3
elif filter_type == "7days":
limit_days = 7
if limit_days:
return job_api_client.get_page_of_jobs(
service_id, page=page, limit_days=limit_days, use_processing_time=True
)
else:
return job_api_client.get_page_of_jobs(service_id, page=page)
@main.route("/activity/services/<uuid:service_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
def all_jobs_activity(service_id):
service_data_retention_days = 7
service_data_retention_days = 8
page = get_page_from_request()
jobs = job_api_client.get_page_of_jobs(service_id, page=page)
jobs = get_filtered_jobs(service_id, page)
all_jobs_dict = generate_job_dict(jobs)
prev_page, next_page, pagination = handle_pagination(jobs, service_id, page)
message_type = ("sms",)
download_availability = get_download_availability(service_id)
download_links = get_download_links(message_type)
return render_template(
"views/activity/all-activity.html",
all_jobs_dict=all_jobs_dict,
@@ -56,51 +143,32 @@ def all_jobs_activity(service_id):
next_page=next_page,
prev_page=prev_page,
pagination=pagination,
total_jobs=jobs.get("total", 0),
**download_availability,
download_link_one_day=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="one_day",
),
download_link_three_day=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="three_day",
),
download_link_five_day=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="five_day",
),
download_link_seven_day=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="seven_day",
),
**download_links,
)
def handle_pagination(jobs, service_id, page):
if page is None:
abort(404, "Invalid page argument ({}).".format(request.args.get("page")))
url_args = {}
if request.args.get("filter"):
url_args["filter"] = request.args.get("filter")
prev_page = (
generate_previous_dict("main.all_jobs_activity", service_id, page)
generate_previous_dict("main.all_jobs_activity", service_id, page, url_args)
if page > 1
else None
)
total_items = jobs.get("total", 0)
page_size = jobs.get("page_size", 50)
total_pages = (total_items + page_size - 1) // page_size
has_next_link = jobs.get("links", {}).get("next") is not None
next_page = (
generate_next_dict("main.all_jobs_activity", service_id, page)
if has_next_link and total_items > 50
generate_next_dict("main.all_jobs_activity", service_id, page, url_args)
if has_next_link and total_items > 50 and page < total_pages
else None
)
pagination = generate_pagination_pages(

View File

@@ -41,7 +41,7 @@ def service_dashboard(service_id):
return redirect(url_for("main.choose_template", service_id=service_id))
job_response = job_api_client.get_jobs(service_id)["data"]
service_data_retention_days = 7
service_data_retention_days = 8
active_jobs = [
job for job in job_response if job["job_status"] != JobStatus.CANCELLED
@@ -55,7 +55,7 @@ def service_dashboard(service_id):
messages_remaining = total_messages.get("messages_remaining", 0)
messages_sent = total_messages.get("messages_sent", 0)
all_statistics = template_statistics_client.get_template_statistics_for_service(
service_id, limit_days=7
service_id, limit_days=8
)
template_statistics = aggregate_template_usage(all_statistics)
return render_template(
@@ -361,7 +361,7 @@ def get_stats_date_range():
current_financial_year = get_current_financial_year()
current_month = get_current_month_for_financial_year(current_financial_year)
start_date = datetime.now().strftime("%Y-%m-%d")
days = 7
days = 8
return {
"current_financial_year": current_financial_year,
"current_month": current_month,

View File

@@ -254,10 +254,13 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
next_page = None
total_items = notifications.get("total", 0)
page_size = notifications.get("page_size", 50)
total_pages = (total_items + page_size - 1) // page_size
if (
"links" in notifications
and notifications["links"].get("next", None)
and total_items > 50
and page < total_pages
):
next_page = generate_next_dict(
"main.view_notifications", service_id, page, url_args

View File

@@ -73,7 +73,9 @@ def invite_user(service_id, user_id=None):
else:
user_to_invite = None
service_has_email_auth = current_service.has_permission(ServicePermission.EMAIL_AUTH)
service_has_email_auth = current_service.has_permission(
ServicePermission.EMAIL_AUTH
)
if not service_has_email_auth:
form.login_authentication.data = "sms_auth"
@@ -116,7 +118,9 @@ def invite_user(service_id, user_id=None):
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>", methods=["GET", "POST"])
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def edit_user_permissions(service_id, user_id):
service_has_email_auth = current_service.has_permission(ServicePermission.EMAIL_AUTH)
service_has_email_auth = current_service.has_permission(
ServicePermission.EMAIL_AUTH
)
user = current_service.get_team_member(user_id)
mobile_number = None

View File

@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from zoneinfo import ZoneInfo
from flask import (
Response,
current_app,
flash,
jsonify,
redirect,
render_template,
request,
stream_with_context,
@@ -14,7 +17,9 @@ from flask import (
from app import current_service, job_api_client, notification_api_client
from app.enums import ServicePermission
from app.main import main
from app.main.views.user_profile import set_timezone
from app.notify_client.api_key_api_client import KEY_TYPE_TEST
from app.s3_client.s3_csv_client import s3download
from app.utils import (
DELIVERED_STATUSES,
FAILURE_STATUSES,
@@ -23,8 +28,10 @@ from app.utils import (
set_status_filters,
)
from app.utils.csv import generate_notifications_csv, get_user_preferred_timezone
from app.utils.s3_csv import convert_s3_csv_timestamps
from app.utils.templates import get_template
from app.utils.user import user_has_permissions
from notifications_utils.s3 import S3ObjectNotFound
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
@@ -94,7 +101,9 @@ def view_notification(service_id, notification_id, error_message=None):
updated_at=notification["sent_at"],
help=get_help_argument(),
notification_id=notification["id"],
can_receive_inbound=(current_service.has_permission(ServicePermission.INBOUND_SMS)),
can_receive_inbound=(
current_service.has_permission(ServicePermission.INBOUND_SMS)
),
sent_with_test_key=(notification.get("key_type") == KEY_TYPE_TEST),
back_link=back_link,
)
@@ -133,23 +142,73 @@ def get_all_personalisation_from_notification(notification):
return notification["personalisation"]
PERIOD_TO_S3_FILENAME = {
"one_day": "1-day-report",
"three_day": "3-day-report",
"five_day": "5-day-report",
"seven_day": "7-day-report",
}
@main.route("/services/<uuid:service_id>/download-notifications.csv")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
def download_notifications_csv(service_id):
set_timezone()
filter_args = parse_filter_args(request.args)
filter_args["status"] = set_status_filters(filter_args)
number_of_days = request.args["number_of_days"]
service_data_retention_days = current_service.get_days_of_retention(
filter_args.get("message_type")[0], number_of_days
)
file_time = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
file_time = f"{file_time} {get_user_preferred_timezone()}"
user_tz_name = get_user_preferred_timezone()
user_tz = ZoneInfo(user_tz_name)
file_time = datetime.now(user_tz).strftime("%Y-%m-%d %I:%M:%S %p")
file_time = f"{file_time} {user_tz_name}"
job_id = request.args.get("job_id")
if not job_id and number_of_days in PERIOD_TO_S3_FILENAME:
try:
s3_report_id = PERIOD_TO_S3_FILENAME[number_of_days]
current_app.logger.info(
f"User is attempting to download {s3_report_id} for service {service_id}"
)
s3_file_content = s3download(service_id, s3_report_id)
return Response(
stream_with_context(
convert_s3_csv_timestamps(
s3_file_content, user_timezone=user_tz_name
)
),
mimetype="text/csv",
headers={
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
file_time,
filter_args["message_type"][0],
current_service.name,
)
},
)
except S3ObjectNotFound:
current_app.logger.warning(
f"File {s3_report_id} was expected but not found for service {service_id}. "
"It may have been deleted after page load."
)
flash(
"The report is no longer available. Please refresh the page.", "default"
)
return redirect(
url_for(
"main.view_notifications",
service_id=service_id,
message_type=filter_args["message_type"][0],
status="sending,delivered,failed",
)
)
return Response(
stream_with_context(
generate_notifications_csv(
service_id=service_id,
job_id=None,
job_id=job_id,
status=filter_args.get("status"),
page=request.args.get("page", 1),
page_size=10000,

View File

@@ -3,17 +3,16 @@ from itertools import groupby
from operator import itemgetter
from statistics import mean
import pytz
from flask import render_template
from app import performance_dashboard_api_client, status_api_client
from app.main import main
from app.utils.csv import get_user_preferred_timezone
from app.utils.csv import get_user_preferred_timezone_obj
@main.route("/performance")
def performance():
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
stats = performance_dashboard_api_client.get_performance_dashboard_stats(
start_date=(datetime.now(preferred_tz) - timedelta(days=7)).date(),
end_date=datetime.now(preferred_tz).date(),

View File

@@ -154,17 +154,23 @@ def set_up_your_profile():
code = request.args.get("code")
state = request.args.get("state")
state_key = f"login-state-{unquote(state)}"
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
if state != stored_state:
flash("Internal error: cannot recognize stored state")
abort(403, "Internal error: cannot recognize stored state #invites")
login_gov_error = request.args.get("error")
if login_gov_error:
flash(f"Login.gov error: {login_gov_error}")
abort(403, f"login_gov_error {login_gov_error} #invites")
if not state:
flash("Login.gov state not detected")
abort(403, "Login.gov state not detected #invites")
state_key = f"login-state-{unquote(state)}"
debug_msg(f"Register tries to fetch state_key {state_key}")
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
if state != stored_state:
flash("Internal error: cannot recognize stored state")
abort(403, "Internal error: cannot recognize stored state #invites")
user_email = redis_client.get(f"user_email-{state}")
user_uuid = redis_client.get(f"user_uuid-{state}")
@@ -181,21 +187,33 @@ def set_up_your_profile():
f"#invites: Got the user_email and user_uuid {user_uuid} from login.gov"
)
invite_data = redis_client.get(f"invitedata-{state}")
# TODO fails here.
invite_data = json.loads(invite_data)
invited_user_id = invite_data["invited_user_id"]
invited_user_email_address = get_invited_user_email_address(invited_user_id)
is_org_invite, invited_user_id, invited_user_email_address = (
process_invited_user(invite_data)
)
current_app.logger.info(
f"#invites: does user email match expected? {user_email == invited_user_email_address}"
)
check_invited_user_email_address_matches_expected(
user_email, invited_user_email_address
)
if is_org_invite:
invited_org_user_accept_invite(invited_user_id)
current_app.logger.info(
f"#invites: accepted org invite user with invited_user_id \
{invited_user_id}"
)
else:
invited_user_accept_invite(invited_user_id)
current_app.logger.info(
f"#invites: accepted invite user with invited_user_id \
{invited_user_id}"
)
invited_user_accept_invite(invited_user_id)
current_app.logger.info(
f"#invites: accepted invite user with invited_user_id \
{invited_user_id} to service {invite_data['service_id']}"
)
# 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(
@@ -235,22 +253,31 @@ def set_up_your_profile():
current_app.logger.info("#invites: going to activate user")
activate_user(user["id"])
current_app.logger.info(f"#invites: activated user with user.id {user['id']}")
usr = User.from_id(user["id"])
usr.add_to_service(
invite_data["service_id"],
invite_data["permissions"],
invite_data["folder_permissions"],
invite_data["from_user_id"],
)
if invite_data.get("service_id"):
usr = User.from_id(user["id"])
# notify-admin-1766
# redirect new users to templates area of new service instead of dashboard
service_id = invite_data["service_id"]
url = url_for(".service_dashboard", service_id=service_id)
url = f"{url}/templates"
current_app.logger.info(f"#invites redirecting to {url}")
return redirect(url)
usr.add_to_service(
invite_data["service_id"],
invite_data["permissions"],
invite_data["folder_permissions"],
invite_data["from_user_id"],
)
# notify-admin-1766
# redirect new users to templates area of new service instead of dashboard
service_id = invite_data["service_id"]
url = url_for(".service_dashboard", service_id=service_id)
url = f"{url}/templates"
current_app.logger.info(f"#invites redirecting to {url}")
return redirect(url)
else:
usr = User.from_id(user["id"])
org_id = invite_data["organization"]
usr.add_to_organization(org_id)
url = url_for(".organization_dashboard", org_id=org_id)
current_app.logger.info(f"#invites redirecting to {url}")
return redirect(url)
# 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
@@ -265,25 +292,67 @@ def get_invited_user_email_address(invited_user_id):
return invited_user.email_address
def get_invited_org_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 = InvitedOrgUser.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 == InvitedUserStatus.EXPIRED:
current_app.logger.error("User invitation has expired")
current_app.logger.error("Service invitation has expired")
flash(
"Your invitation has expired; please contact the person who invited you for additional help."
"Your service invitation has expired; please contact the person who invited you for additional help."
)
abort(401, "Your invitation has expired #invites")
abort(401, "Your service invitation has expired #invites")
if invited_user.status == InvitedUserStatus.CANCELLED:
current_app.logger.error("User invitation has been cancelled")
current_app.logger.error("Service invitation has been cancelled")
flash(
"Your invitation is no longer valid; please contact the person who invited you for additional help."
"Your service invitation is no longer valid; please contact the person who invited you for additional help."
)
abort(401, "Your invitation was canceled #invites")
abort(401, "Your service invitation was canceled #invites")
invited_user.accept_invite()
def invited_org_user_accept_invite(invited_user_id):
invited_user = InvitedOrgUser.by_id(invited_user_id)
if invited_user.status == InvitedUserStatus.EXPIRED:
current_app.logger.error("Organization invitation has expired")
flash(
"Your organization invitation has expired; please contact the person who invited you for additional help."
)
abort(401, "Your organization invitation has expired #invites")
if invited_user.status == InvitedUserStatus.CANCELLED:
current_app.logger.error("Organization invitation has been cancelled")
flash(
"Your organization invitation is no longer valid; \
please contact the person who invited you for additional help."
)
abort(401, "Your organization invitation was canceled #invites")
invited_user.accept_invite()
def debug_msg(msg):
current_app.logger.debug(hilite(msg))
def process_invited_user(invite_data):
is_org_invite = False
if invite_data.get("invited_user_id"):
invited_user_id = invite_data["invited_user_id"]
invited_user_email_address = get_invited_user_email_address(invited_user_id)
else:
invited_user_id = invite_data["id"]
is_org_invite = True
invited_user_email_address = get_invited_org_user_email_address(invited_user_id)
return is_org_invite, invited_user_id, invited_user_email_address

View File

@@ -560,7 +560,9 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
max_errors_shown=50,
guestlist=allow_list,
remaining_messages=remaining_messages,
allow_international_sms=current_service.has_permission(ServicePermission.INTERNATIONAL_SMS),
allow_international_sms=current_service.has_permission(
ServicePermission.INTERNATIONAL_SMS
),
)
if request.args.get("from_test"):

View File

@@ -146,6 +146,9 @@ def _do_login_dot_gov(): # $ pragma: no cover
# return verify_email(user, redirect_url)
usr = User.from_email_address(user["email_address"])
# New users on the invite path need activation
# if not usr.is_active:
current_app.logger.info(f"activating user {usr.id} #notify-admin-1505")
activate_user(usr.id)
except BaseException as be: # noqa B036

View File

@@ -27,7 +27,7 @@ def _sign_out_at_login_dot_gov():
def sign_out():
if current_user.is_authenticated:
current_user.deactivate()
# current_user.deactivate()
session.clear()
current_user.sign_out()

View File

@@ -180,7 +180,10 @@ def process_folder_management_form(form, current_folder_id):
if form.is_add_folder_op:
new_folder_id = template_folder_api_client.create_template_folder(
current_service.id, name=form.get_folder_name(), parent_id=current_folder_id
current_service.id,
name=form.get_folder_name(),
parent_id=current_folder_id,
created_by_id=str(current_user.id),
)
if form.is_move_op:

View File

@@ -202,7 +202,6 @@ class User(JSONModel, UserMixin):
@property
def is_gov_user(self):
is_gov = is_gov_user(self.email_address)
# current_app.logger.info(f"User {self.id} is_gov_user: {is_gov}")
return is_gov
@property
@@ -211,9 +210,6 @@ class User(JSONModel, UserMixin):
@property
def platform_admin(self):
# current_app.logger.warning(
# f"Checking User {self.id} for platform admin: {self._platform_admin}"
# )
return self._platform_admin and not session.get(
"disable_platform_admin_view", False
)
@@ -243,14 +239,10 @@ class User(JSONModel, UserMixin):
# we shouldn't have any pages that require permissions, but don't specify a service or organization.
# use @user_is_platform_admin for platform admin only pages
# raise NotImplementedError
# current_app.logger.warning(f"VIEW ARGS ARE {request.view_args}")
pass
# platform admins should be able to do most things (except eg send messages, or create api keys)
if self.platform_admin and not restrict_admin_usage:
current_app.logger.debug(
"has_permissions is true because user is platform_admin"
)
return True
if org_id:
@@ -564,17 +556,11 @@ class InvitedUser(JSONModel):
return cls.by_id(invited_user_id) if invited_user_id else None
def has_permissions(self, *permissions):
# current_app.logger.warning(
# f"Checking invited user {self.id} for permissions: {permissions}"
# )
if self.status == InvitedUserStatus.CANCELLED:
return False
return set(self.permissions) > set(permissions)
def has_permission_for_service(self, service_id, permission):
# current_app.logger.warn(
# f"Checking invited user {self.id} for permission: {permission} on service {service_id}"
# )
if self.status == InvitedUserStatus.CANCELLED:
return False
return self.service == service_id and permission in self.permissions

View File

@@ -32,12 +32,22 @@ class JobApiClient(NotifyAdminAPIClient):
return job
def get_jobs(self, service_id, *, limit_days=None, statuses=None, page=1):
def get_jobs(
self,
service_id,
*,
limit_days=None,
statuses=None,
page=1,
use_processing_time=False,
):
params = {"page": page}
if limit_days is not None:
params["limit_days"] = limit_days
if statuses is not None:
params["statuses"] = ",".join(statuses)
if use_processing_time:
params["use_processing_time"] = "true"
job = self.get(url=f"/service/{service_id}/job", params=params)
return job
@@ -61,12 +71,21 @@ class JobApiClient(NotifyAdminAPIClient):
if job["job_status"] != JobStatus.CANCELLED
)
def get_page_of_jobs(self, service_id, *, page, statuses=None, limit_days=None):
def get_page_of_jobs(
self,
service_id,
*,
page,
statuses=None,
limit_days=None,
use_processing_time=False,
):
return self.get_jobs(
service_id,
statuses=statuses or self.NON_SCHEDULED_JOB_STATUSES,
page=page,
limit_days=limit_days,
use_processing_time=use_processing_time,
)
def get_immediate_jobs(self, service_id):

View File

@@ -1,5 +1,13 @@
import json
import secrets
from urllib.parse import unquote
from flask import current_app, request
from app import redis_client
from app.enums import InvitedOrgUserStatus
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
from notifications_utils.url_safe_token import generate_token
class OrgInviteApiClient(NotifyAdminAPIClient):
@@ -15,7 +23,37 @@ class OrgInviteApiClient(NotifyAdminAPIClient):
"invite_link_host": self.admin_url,
}
data = _attach_current_user(data)
ttl = 24 * 60 * 60
# make and store the state
state = generate_token(
str(request.remote_addr),
current_app.config["SECRET_KEY"],
current_app.config["DANGEROUS_SALT"],
)
state_key = f"login-state-{unquote(state)}"
redis_client.set(state_key, state, ex=ttl)
# make and store the nonce
nonce = secrets.token_urlsafe()
nonce_key = f"login-nonce-{unquote(nonce)}"
redis_client.set(nonce_key, nonce, ex=ttl) # save the nonce to redis.
data["nonce"] = nonce # This is passed to api for the invite url.
data["state"] = state # This is passed to api for the invite url.
resp = self.post(url="/organization/{}/invite".format(org_id), data=data)
invite_data_key = f"invitedata-{unquote(state)}"
# For historical reasons 'invite' signifies a service invite
# and 'data' signifies an org invite
if resp.get("invite"):
redis_invite_data = resp["invite"]
else:
redis_invite_data = resp["data"]
redis_invite_data = json.dumps(redis_invite_data)
redis_client.set(invite_data_key, redis_invite_data, ex=ttl)
return resp["data"]
def get_invites_for_organization(self, org_id):

View File

@@ -4,8 +4,12 @@ from app.notify_client import NotifyAdminAPIClient, cache
class TemplateFolderAPIClient(NotifyAdminAPIClient):
@cache.delete("service-{service_id}-template-folders")
def create_template_folder(self, service_id, name, parent_id=None):
def create_template_folder(
self, service_id, name, parent_id=None, created_by_id=None
):
data = {"name": name, "parent_id": parent_id}
if created_by_id:
data["created_by_id"] = created_by_id
return self.post("/service/{}/template-folder".format(service_id), data)[
"data"
]["id"]

View File

@@ -5,6 +5,8 @@ from boto3 import Session
from botocore.config import Config
from flask import current_app
from notifications_utils.s3 import S3ObjectNotFound
AWS_CLIENT_CONFIG = Config(
# This config is required to enable S3 to connect to FIPS-enabled
# endpoints. See https://aws.amazon.com/compliance/fips/ for more
@@ -48,6 +50,19 @@ def get_s3_object(
return obj
def check_s3_file_exists(obj):
try:
obj.load()
return True
except botocore.exceptions.ClientError as client_error:
error_code = client_error.response["Error"]["Code"]
if error_code in ["404", "NoSuchKey"]:
return False
return False
except Exception:
return False
def get_s3_metadata(obj):
try:
return obj.get()["Metadata"]
@@ -55,6 +70,8 @@ def get_s3_metadata(obj):
current_app.logger.error(
f"Unable to download s3 file {obj.bucket_name}/{obj.key}"
)
if client_error.response["Error"]["Code"] == "NoSuchKey":
raise S3ObjectNotFound(client_error.response, client_error.operation_name)
raise client_error
@@ -69,12 +86,13 @@ def set_s3_metadata(obj, **kwargs):
def get_s3_contents(obj):
contents = ""
try:
contents = obj.get()["Body"].read().decode("utf-8")
response = obj.get()
return response["Body"].read().decode("utf-8")
except botocore.exceptions.ClientError as client_error:
current_app.logger.error(
f"Unable to download s3 file {obj.bucket_name}/{obj.key}"
)
if client_error.response["Error"]["Code"] == "NoSuchKey":
raise S3ObjectNotFound(client_error.response, client_error.operation_name)
raise client_error
return contents

View File

@@ -4,6 +4,7 @@ import uuid
from flask import current_app
from app.s3_client import (
check_s3_file_exists,
get_s3_contents,
get_s3_metadata,
get_s3_object,
@@ -15,12 +16,16 @@ NEW_FILE_LOCATION_STRUCTURE = "{}-service-notify/{}.csv"
def get_csv_location(service_id, upload_id):
bucket = current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
key = NEW_FILE_LOCATION_STRUCTURE.format(service_id, upload_id)
region = current_app.config["CSV_UPLOAD_BUCKET"]["region"]
return (
current_app.config["CSV_UPLOAD_BUCKET"]["bucket"],
NEW_FILE_LOCATION_STRUCTURE.format(service_id, upload_id),
bucket,
key,
current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"],
current_app.config["CSV_UPLOAD_BUCKET"]["secret_access_key"],
current_app.config["CSV_UPLOAD_BUCKET"]["region"],
region,
)
@@ -72,3 +77,7 @@ def set_metadata_on_csv_upload(service_id, upload_id, **kwargs):
def get_csv_metadata(service_id, upload_id):
return get_s3_metadata(get_csv_upload(service_id, upload_id))
def check_s3_report_exists(service_id, upload_id):
return check_s3_file_exists(get_csv_upload(service_id, upload_id))

View File

@@ -1,10 +1,9 @@
from datetime import datetime
from functools import reduce
import pytz
from dateutil import parser
from app.utils.csv import get_user_preferred_timezone
from app.utils.csv import get_user_preferred_timezone_obj
def sum_of_statistics(delivery_statistics):
@@ -27,7 +26,7 @@ def sum_of_statistics(delivery_statistics):
def add_rates_to(delivery_statistics):
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
return dict(
emails_failure_rate=get_formatted_percentage(
delivery_statistics["emails_failed"],

View File

@@ -1,14 +1,3 @@
{% macro ajax_block(partials, url, key, finished=False, form='') %}
{% if not finished %}
<div
data-module="update-content"
data-resource="{{ url }}"
data-key="{{ key }}"
data-form="{{ form }}"
>
{% endif %}
{{ partials[key]|safe }}
{% if not finished %}
</div>
{% endif %}
{{ partials[key]|safe }}
{% endmacro %}

View File

@@ -0,0 +1,20 @@
{% from "components/select-input.html" import select, select_list, select_nested, select_wrapper, select_input %}
{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False) %}
{{ select(field, hint, disable, option_hints, hide_legend, input="checkbox") }}
{% endmacro %}
{% macro checkbox_list(options, child_map, disable=[], option_hints={}) %}
{{ select_list(options, child_map, disable, option_hints, input="checkbox") }}
{% endmacro %}
{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False) %}
{{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, input="checkbox") }}
{% endmacro %}
{% macro checkbox(option, disable=[], option_hints={}, data_target=None, as_list_item=False) %}
{{ select_input(option, disable, option_hints, data_target, as_list_item, input="checkbox") }}
{% endmacro %}

View File

@@ -17,7 +17,8 @@
{% call govukFieldset({
"legend": {
"text": field.label.text,
"isPageHeading": False
"isPageHeading": False,
"classes": "text-bold"
},
"attributes": attributes
})
@@ -36,9 +37,9 @@
{% set autocomplete = "" %}
{% endif %}
{% if entry.errors %}
{% set label_classes = "usa-input--numbered__label usa-input--numbered__label--error" %}
{% set label_classes = "usa-label usa-label--error" %}
{% else %}
{% set label_classes = "usa-input--numbered__label" %}
{% set label_classes = "usa-label" %}
{% endif %}
{% set field_name = field.name + "-" + loop.index|string %}
{{ entry(param_extensions={
@@ -48,7 +49,7 @@
"html": '<span class="usa-sr-only">' + item_name + ' number </span>' + loop.index|string + '.',
"classes": label_classes
},
"classes": "usa-input--numbered ",
"classes": "usa-input",
"value": field.data[loop.index0],
"autocomplete": autocomplete
}) }}

View File

@@ -1,30 +1,30 @@
{% macro previous_next_navigation(previous_page, next_page) %}
{% if previous_page or next_page %}
<nav class="govuk-previous-and-next-navigation" role="navigation" aria-label="Pagination">
<ul class="group">
<nav class="usa-pagination margin-y-5 text-left" role="navigation" aria-label="Pagination">
<ul class="usa-pagination__list flex-justify-start">
{% if previous_page %}
<li class="previous-page">
<a class="usa-link" href="{{previous_page['url']}}" rel="previous" >
<span class="pagination-part-title">
<svg class="pagination-icon" xmlns="http://www.w3.org/2000/svg" height="13" width="17" viewBox="0 0 17 13">
<path fill="currentColor" d="m6.5938-0.0078125-6.7266 6.7266 6.7441 6.4062 1.377-1.449-4.1856-3.9768h12.896v-2h-12.984l4.2931-4.293-1.414-1.414z"/>
</svg>
<li class="usa-pagination__item usa-pagination__arrow">
<a class="usa-pagination__link usa-pagination__previous-page" href="{{previous_page['url']}}" rel="previous">
<svg class="usa-icon" xmlns="http://www.w3.org/2000/svg" height="13" width="17" viewBox="0 0 17 13">
<path fill="currentColor" d="m6.5938-0.0078125-6.7266 6.7266 6.7441 6.4062 1.377-1.449-4.1856-3.9768h12.896v-2h-12.984l4.2931-4.293-1.414-1.414z"/>
</svg>
<span class="usa-pagination__link-text">
{{previous_page['title']}}
</span>
<span class="pagination-label">{{previous_page['label']}}</span>
<span class="usa-sr-only">{{previous_page['label']}}</span>
</a>
</li>
{% endif %}
{% if next_page %}
<li class="next-page">
<a class="usa-link" href="{{next_page['url']}}" rel="next">
<span class="pagination-part-title">
<li class="usa-pagination__item usa-pagination__arrow">
<a class="usa-pagination__link usa-pagination__next-page" href="{{next_page['url']}}" rel="next">
<span class="usa-pagination__link-text">
{{next_page['title']}}
<svg class="pagination-icon" xmlns="http://www.w3.org/2000/svg" height="13" width="17" viewBox="0 0 17 13">
<path fill="currentColor" d="m10.107-0.0078125-1.4136 1.414 4.2926 4.293h-12.986v2h12.896l-4.1855 3.9766 1.377 1.4492 6.7441-6.4062-6.7246-6.7266z"/>
</svg>
</span>
<span class="pagination-label">{{next_page['label']}}</span>
<svg class="usa-icon" xmlns="http://www.w3.org/2000/svg" height="13" width="17" viewBox="0 0 17 13">
<path fill="currentColor" d="m10.107-0.0078125-1.4136 1.414 4.2926 4.293h-12.986v2h12.896l-4.1855 3.9766 1.377 1.4492 6.7441-6.4062-6.7246-6.7266z"/>
</svg>
<span class="usa-sr-only">{{next_page['label']}}</span>
</a>
</li>
{% endif %}

View File

@@ -1,7 +1,7 @@
{% from "components/select-input.html" import select, select_list, select_nested, select_wrapper, select_input %}
{% macro radios(field, hint=None, disable=[], option_hints={}, hide_legend=False, inline=False) %}
{{ select(field, hint, disable, option_hints, hide_legend, input="radio", inline=inline) }}
{% macro radios(field, hint=None, disable=[], option_hints={}, hide_legend=False, inline=False, legend_style="text") %}
{{ select(field, hint, disable, option_hints, hide_legend, legend_style=legend_style, input="radio", inline=inline) }}
{% endmacro %}

View File

@@ -12,12 +12,12 @@
{% macro select_list(options, child_map, disable=[], option_hints={}, input="radio", indent_level=0) %}
{% for option in options %}
{% if child_map[option.data] %}
<div class="{% if indent_level > 0 %}margin-left-{{ indent_level * 4 }}{% endif %}">
<div class="{% if indent_level == 1 %}margin-left-3{% elif indent_level == 2 %}margin-left-4{% elif indent_level >= 3 %}margin-left-5{% endif %}">
{{ select_input(option, disable, option_hints, as_list_item=False, input=input) }}
</div>
{{ select_list(child_map[option.data], child_map, disable, option_hints, input=input, indent_level=indent_level + 1) }}
{% else %}
<div class="{% if indent_level > 0 %}margin-left-{{ indent_level * 4 }}{% endif %}">
<div class="{% if indent_level == 1 %}margin-left-3{% elif indent_level == 2 %}margin-left-4{% elif indent_level >= 3 %}margin-left-5{% endif %}">
{{ select_input(option, disable, option_hints, as_list_item=False, input=input) }}
</div>
{% endif %}
@@ -45,7 +45,11 @@
<fieldset id="{{ field.id }}" class="usa-fieldset" {% if inline %}class="inline"{% endif %}>
<legend class="{{ 'usa-legend' if not hide_legend else '' }}{% if legend_style != 'text' %} {{ legend_style }}{% endif %}">
{% if hide_legend %}<span class="usa-sr-only">{% endif %}
{{ field.label.text|safe }}
{% if legend_style == 'usa-legend--large' %}
<h1 class="usa-legend font-sans-lg">{{ field.label.text|safe }}</h1>
{% else %}
{{ field.label.text|safe }}
{% endif %}
{% if hide_legend %}</span>{% endif %}
{% if hint %}
<span class="form-hint">
@@ -64,9 +68,9 @@
{% endmacro %}
{% macro select_input(option, disable=[], option_hints={}, data_target=None, as_list_item=False, input="radio") %}
<div class="usa-radio" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
<div class="usa-{{ input }}" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
<input
id="{{ option.id }}" class="usa-radio__input" name="{{ option.name }}" type="{{ input }}" value="{{ option.data }}"
id="{{ option.id }}" class="usa-{{ input }}__input" name="{{ option.name }}" type="{{ input }}" value="{{ option.data }}"
{% if option.data in disable %}
disabled
{% endif %}
@@ -74,7 +78,7 @@
checked
{% endif %}
>
<label class="usa-radio__label" for="{{ option.id }}">
<label class="usa-{{ input }}__label" for="{{ option.id }}">
{{ option.label.text }}
{% if option_hints[option.data] %}
<span class="usa-hint">

View File

@@ -25,7 +25,7 @@
<p>Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users
can create an account and get started within minutes. We take the security and privacy of messaging data seriously
by minimizing data retention and using modern encryption methods.</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&amp;start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> <h2 class="padding-bottom-3">Product Highlights</h2>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&amp;start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen credentialless></iframe> <h2 class="padding-bottom-3">Product Highlights</h2>
{% set product_highlights = [
{
"svg_src": "#send",

View File

@@ -30,7 +30,7 @@
</li>
{% else %}
<li class="usa-pagination__item">
<a class="usa-pagination__button" href="?page={{ page }}">
<a class="usa-pagination__button" href="?page={{ page }}{% if request.args.get('filter') %}&filter={{ request.args.get('filter') }}{% endif %}">
{{ page }}
</a>
</li>
@@ -51,14 +51,62 @@
{% endif %}
</ul>
</nav>
{% if pagination and total_jobs %}
<p class="text-center font-body-sm">
Page <span class="text-bold">{{ pagination.current }}</span> of <span class="text-bold">{{ pagination.last }}</span> (<span>{{ total_jobs }}</span> total jobs)
</p>
{% endif %}
{% endif %}
{% endset %}
{% block maincolumn_content %}
<style>
.download-reports-container .usa-button {
padding: 0.75rem 1rem;
min-height: 48px;
justify-content: flex-start;
}
.download-reports-container .usa-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
</style>
<div class="margin-bottom-8">
<h1 class="usa-sr-only">All activity</h1>
<h2 class="font-body-2xl line-height-sans-2 margin-0">All activity</h2>
<h2 class="margin-top-4 margin-bottom-1">Sent jobs</h2>
<div class="usa-table-container--scrollable-mobile">
<div class="flex-wrap gap-3 display-flex flex-align-start margin-top-2">
<div class="flex-1">
<ul class="usa-button-group usa-button-group--segmented">
<li class="usa-button-group__item">
<a href="{{ url_for('main.all_jobs_activity', service_id=current_service.id) }}"
class="usa-button usa-button--small {% if not request.args.get('filter') %}{% else %}usa-button--outline{% endif %}">
All
</a>
</li>
<li class="usa-button-group__item">
<a href="{{ url_for('main.all_jobs_activity', service_id=current_service.id, filter='24hours') }}"
class="usa-button usa-button--small {% if request.args.get('filter') == '24hours' %}{% else %}usa-button--outline{% endif %}">
Last 24 hours
</a>
</li>
<li class="usa-button-group__item">
<a href="{{ url_for('main.all_jobs_activity', service_id=current_service.id, filter='3days') }}"
class="usa-button usa-button--small {% if request.args.get('filter') == '3days' %}{% else %}usa-button--outline{% endif %}">
Last 3 days
</a>
</li>
<li class="usa-button-group__item">
<a href="{{ url_for('main.all_jobs_activity', service_id=current_service.id, filter='7days') }}"
class="usa-button usa-button--small {% if request.args.get('filter') == '7days' %}{% else %}usa-button--outline{% endif %}">
Last 7 days
</a>
</li>
</ul>
</div>
</div>
<div class="usa-table-container--scrollable-mobile table-overflow-x-auto">
<table class="usa-table usa-table--compact job-table">
<caption class="usa-sr-only">Table showing all sent jobs for this service</caption>
<thead class="table-field-headings">
@@ -66,23 +114,20 @@
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId">
<span>Job ID#</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Template</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Started</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Sender</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<th scope="col" role="columnheader" class="table-field-heading">
<span>Report</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Delivered</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Failed</span>
<th scope="col" role="columnheader" class="table-field-heading">
<span>Status</span>
</th>
</tr>
</thead>
@@ -90,28 +135,34 @@
{% if all_jobs_dict %}
{% for job in all_jobs_dict %}
<tr class="table-row">
<td class="table-field jobid" role="rowheader">
<td class="table-field width-5" role="rowheader">
<a class="usa-link" href="{{ job.view_job_link }}">
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
</a>
</td>
<td class="table-field template">{{ job.template_name }}</td>
<td data-sort-value="{{ job.activity_time | convert_time_unixtimestamp }}" class="table-field time-sent">
<td class="table-field width-16">{{ job.template_name }}</td>
<td data-sort-value="{{ job.activity_time | convert_time_unixtimestamp }}" class="table-field width-18">
{{ job.activity_time|format_datetime_table }}
</td>
<td class="table-field sender">{{ job.created_by.name }}</td>
<td class="table-field report">
<td class="table-field width-30 ">{{ job.created_by.name }}</td>
<td class="text-center table-field width-2">
{% if job.can_download %}
<a href="{{ job.download_link }}">
<img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="">
<img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="" class="padding-top-05">
<span class="usa-sr-only">Download report link</span>
</a>
{% else %}
<span>N/A</span>
{% endif %}
</td>
<td class="table-field delivered">{{ job.delivered_count if job.delivered_count is not none else '0' }}</td>
<td class="table-field failed">{{ job.failed_count if job.failed_count is not none else '0' }}</td>
<td class="table-field width-15">
<span class="bg-base-lighter padding-x-05 padding-y-0 font-body-3xs text-bold radius-sm margin-right-05 display-inline-block text-nowrap">
{{ job.delivered_count if job.delivered_count is not none else '0' }} delivered
</span>
<span class="bg-base-lighter padding-x-05 padding-y-0 font-body-3xs text-bold radius-sm display-inline-block text-nowrap">
{{ job.failed_count if job.failed_count is not none else '0' }} failed
</span>
</td>
</tr>
{% endfor %}
{% else %}
@@ -122,37 +173,104 @@
</tbody>
</table>
<div class="usa-sr-only usa-table__announcement-region" aria-live="polite"></div>
<p><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
<p class="font-body-sm"><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
</div>
{{show_pagination}}
{% if current_user.has_permissions(ServicePermission.VIEW_ACTIVITY) %}
{% if has_any_download_data %}
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Download recent reports</h2>
{% if has_1_day_data %}
<p class="font-body-sm">
<a href="{{ download_link_one_day }}" download="download" class="usa-link">Download all data last 24 hours (<abbr title="Comma separated values">CSV</abbr>)</a>
</p>
{% endif %}
{% if has_3_day_data %}
<p class="font-body-sm">
<a href="{{ download_link_three_day }}" download="download" class="usa-link">Download all data last 3 days (<abbr title="Comma separated values">CSV</abbr>)</a>
&emsp;
</p>
{% endif %}
{% if has_5_day_data %}
<p class="font-body-sm">
<a href="{{ download_link_five_day }}" download="download" class="usa-link">Download all data last 5 days (<abbr title="Comma separated values">CSV</abbr>)</a>
</p>
{% endif %}
{% if has_7_day_data %}
<p class="font-body-sm">
<a href="{{ download_link_seven_day }}" download="download" class="usa-link">Download all data last 7 days (<abbr title="Comma separated values">CSV</abbr>)</a>
</p>
{% endif %}
{% else %}
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Download recent reports</h2>
<p class="font-body-sm">No recent activity to download. Download links will appear when jobs are available.</p>
{% endif %}
<div class="usa-summary-box margin-top-4" role="region" aria-labelledby="download-reports-heading">
<div class="usa-summary-box__body">
<h2 class="usa-summary-box__heading" id="download-reports-heading">Download recent reports</h2>
<div class="usa-summary-box__text">
<p class="font-body-xs margin-bottom-3">
Reports are automatically generated daily at midnight and include data through the previous day.
Today's activity will appear in tomorrow's report.
</p>
<div class="download-reports-container maxw-tablet-lg">
<div class="margin-bottom-2">
{% if report_1_day.available %}
<a href="{{ download_link_one_day }}"
class="usa-button width-full display-flex flex-align-center"
download
aria-label="Download yesterday's report, CSV format, {{ report_1_day.size }}">
<svg class="usa-icon margin-right-2" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{{ asset_url('img/sprite.svg') }}#file_download"></use>
</svg>
<span>Yesterday&nbsp; - {{ report_1_day.size }}</span>
</a>
{% else %}
<button class="usa-button width-full"
disabled
aria-label="Yesterday's report not available - no messages were sent">
Yesterday&nbsp; - &nbsp;No messages sent
</button>
{% endif %}
</div>
<div class="margin-bottom-2">
{% if report_3_day.available %}
<a href="{{ download_link_three_day }}"
class="usa-button width-full display-flex flex-align-center"
download
aria-label="Download last 3 days report, CSV format, {{ report_3_day.size }}">
<svg class="usa-icon margin-right-2" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{{ asset_url('img/sprite.svg') }}#file_download"></use>
</svg>
<span>Last 3 days - {{ report_3_day.size }}</span>
</a>
{% else %}
<button class="usa-button width-full"
disabled
aria-label="Last 3 days report not available - no messages were sent">
Last 3 days - No messages sent
</button>
{% endif %}
</div>
<div class="margin-bottom-2">
{% if report_5_day.available %}
<a href="{{ download_link_five_day }}"
class="usa-button width-full display-flex flex-align-center"
download
aria-label="Download last 5 days report, CSV format, {{ report_5_day.size }}">
<svg class="usa-icon margin-right-2" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{{ asset_url('img/sprite.svg') }}#file_download"></use>
</svg>
<span>Last 5 days - {{ report_5_day.size }}</span>
</a>
{% else %}
<button class="usa-button width-full"
disabled
aria-label="Last 5 days report not available - no messages were sent">
Last 5 days - No messages sent
</button>
{% endif %}
</div>
<div class="margin-bottom-2">
{% if report_7_day.available %}
<a href="{{ download_link_seven_day }}"
class="usa-button width-full display-flex flex-align-center"
download
aria-label="Download last 7 days report, CSV format, {{ report_7_day.size }}">
<svg class="usa-icon margin-right-2" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{{ asset_url('img/sprite.svg') }}#file_download"></use>
</svg>
<span>Last 7 days - {{ report_7_day.size }}</span>
</a>
{% else %}
<button class="usa-button width-full"
disabled
aria-label="Last 7 days report not available - no messages were sent">
Last 7 days - No messages sent
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -59,11 +59,13 @@
autocomplete=False
) }}
{{ list_entry(
form.phone_numbers,
item_name='phone number',
autocomplete=False
) }}
<div class="margin-top-4">
{{ list_entry(
form.phone_numbers,
item_name='phone number',
autocomplete=False
) }}
</div>
</div>
</div>

View File

@@ -4,6 +4,7 @@
{% from "components/banner.html" import banner_wrapper %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/radios.html" import radios %}
{% block service_page_title %}
Create an API key
@@ -19,7 +20,17 @@
{% call form_wrapper() %}
{{ form.key_name }}
{{ form.key_type }}
<div class="margin-top-5">
{% if current_service.trial_mode %}
{{ radios(
form.key_type,
disable=['normal'],
option_hints={'normal': 'Not available because your service is in <a class="usa-link" href="/features/trial-mode">trial mode</a>'|safe}
) }}
{% else %}
{{ radios(form.key_type) }}
{% endif %}
</div>
{{ page_footer('Continue') }}
{% endcall %}

View File

@@ -101,9 +101,16 @@
{% set button_text %}
{{ "Schedule" if scheduled_for else 'Send'}}
{% endset %}
{{ usaButton({
"text": button_text,
"name": button_text
}) }}
<div>
{{ usaButton({
"text": button_text,
"name": button_text
}) }}
{{ usaButton({
"text": "Change CSV file",
"classes": "usa-button--outline",
"href": url_for('main.send_messages', service_id=current_service.id, template_id=template_id)
}) }}
</div>
</form>
{% endblock %}

View File

@@ -10,7 +10,7 @@
</form>
<div id="activityChart">
<div class="chart-header">
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</div>
<div class="chart-subtitle">{{ current_service.name }}</div>
<div class="chart-legend" role="region" aria-label="Legend"></div>
</div>
<div class="chart-container" id="weeklyChart"></div>
@@ -22,15 +22,15 @@
<div id="tableActivity" class="table-overflow-x-auto">
<h2 id="table-heading" class="margin-top-4 margin-bottom-1">Service activity</h2>
<table class="usa-table job-table" id="activity-table">
<table class="usa-table usa-table--compact job-table" id="activity-table">
<caption class="usa-sr-only">Table showing the sent jobs for {{current_service.name}}</caption>
<thead class="table-field-headings">
<tr>
<th scope="col" class="table-field-heading-first" id="jobId">Job ID#</th>
<th data-sortable scope="col" class="table-field-heading" scope="col">Template</th>
<th data-sortable scope="col" class="table-field-heading">Job status</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading sender-column">Sender
<th data-sortable scope="col" class="table-field-heading width-30">Job status</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading" data-column="sender">Sender
</th>
<th data-sortable scope="col" class="table-field-heading"># of Recipients</th>
</tr>
@@ -39,13 +39,13 @@
{% if jobs %}
{% for job in jobs %}
<tr id="{{ job.id }}">
<td class="table-field jobid" role="rowheader">
<td class="table-field" role="rowheader">
<a class="usa-link" href="{{ url_for('.view_job', service_id=current_service.id, job_id=job.id )}}">
{{ job.id[:8] if job.id else 'Manually entered number' }}
</a>
</td>
<td class="table-field template">{{ job.template_name }}</td>
<td class="table-field time-sent">
<td class="table-field width-35">{{ job.template_name }}</td>
<td class="table-field width-20">
{% if not job.finished_processing %}
{% if job.scheduled_for%}
Scheduled for {{ job.scheduled_for|format_datetime_table }}
@@ -58,8 +58,8 @@
Sent on {{ job.processing_started|format_datetime_table }}
{% endif %}
</td>
<td class="table-field sender sender-column">{{ job.created_by.name }}</td>
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
<td class="table-field" data-sender="{{ job.created_by.name }}">{{ job.created_by.name }}</td>
<td class="table-field width-5">{{ job.notification_count }}</td>
</tr>
{% endfor %}
{% else %}

View File

@@ -3,6 +3,7 @@
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/radios.html" import radios %}
{% block per_page_title %}
Set auth type for {{ user.name }}
@@ -19,7 +20,9 @@
{{ page_header('Set auth type for ' + user.name) }}
{% call form_wrapper() %}
{{ form.auth_type }}
<div class="auth-type-radios">
{{ radios(form.auth_type) }}
</div>
{{ page_footer('Save') }}
{% endcall %}
</div>

View File

@@ -15,56 +15,44 @@
<p class="max-width-full">This page refreshes automatically to show the latest message activity delivery rates, details, and reports.<br>You can watch it in progress or check back later.</p>
{% endif %}
<div data-job-id="{{ job.id }}" data-feature="{{FEATURE_SOCKET_ENABLED | lower}}" data-host="{{ api_public_url }}">
{% if not job.finished_processing %}
{% if not job.finished_processing and FEATURE_SOCKET_ENABLED %}
<div
data-resource="{{ updates_url }}"
{% if FEATURE_SOCKET_ENABLED %}
data-socket-update="status"
{% else %}
data-module="update-content"
{% endif %}
data-socket-update="status"
data-key="status"
data-form=""
>
{% endif %}
{{ partials['status']|safe }}
{% if not job.finished_processing %}
{% if not job.finished_processing and FEATURE_SOCKET_ENABLED %}
</div>
{% endif %}
{% if not finished %}
{% if not finished and FEATURE_SOCKET_ENABLED %}
<div
data-resource="{{ updates_url }}"
{% if FEATURE_SOCKET_ENABLED %}
data-socket-update="counts"
{% else %}
data-module="update-content"
{% endif %}
data-socket-update="counts"
data-key="counts"
data-form=""
>
{% endif %}
{{ partials['counts']|safe }}
{% if not finished %}
{% if not finished and FEATURE_SOCKET_ENABLED %}
</div>
{% endif %}
<p class="notification-status max-width-full">
Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear back from the carrier they have received it and attempted deliver. More information on <a class="usa-link" href="{{ url_for('main.message_status') }}">delivery status</a>.
</p>
{% if not job.processing_finished %}
{% if not job.processing_finished and FEATURE_SOCKET_ENABLED %}
<div
data-resource="{{ updates_url }}"
{% if FEATURE_SOCKET_ENABLED %}
data-socket-update="notifications"
{% else %}
data-module="update-content"
{% endif %}
data-socket-update="notifications"
data-key="notifications"
data-form=""
>
{% endif %}
{{ partials['notifications']|safe }}
{% if not job.processing_finished %}
{% if not job.processing_finished and FEATURE_SOCKET_ENABLED %}
</div>
{% endif %}
</div>

View File

@@ -86,13 +86,17 @@
</ul>
{% if current_service.all_template_folders %}
<p class="usa-body tick-cross-list-hint">
{% set folder_count = user.template_folders_for_service(current_service) | length %}
{% if folder_count == 0 %}
Cannot see any folders
{% elif folder_count != current_service.all_template_folders | length %}
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
{% if user.platform_admin %}
Platform admin can see all folders
{% else %}
Can see all folders
{% set folder_count = user.template_folders_for_service(current_service) | length %}
{% if folder_count == 0 %}
Cannot see any folders
{% elif folder_count != current_service.all_template_folders | length %}
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
{% else %}
Can see all folders
{% endif %}
{% endif %}
</p>
{% endif %}

View File

@@ -11,13 +11,15 @@
{% endif %}
{% if service_has_email_auth %}
{% if not mobile_number %}
{{ radios(
form.login_authentication,
disable=['sms_auth'],
option_hints={'sms_auth': 'Not available because this team member has not added a phone&nbsp;number to their profile'|safe}
) }}
{% else %}
{{ radios(form.login_authentication) }}
{% endif %}
<div class="login-auth-radios">
{% if not mobile_number %}
{{ radios(
form.login_authentication,
disable=['sms_auth'],
option_hints={'sms_auth': 'Not available because this team member has not added a phone&nbsp;number to their profile'|safe}
) }}
{% else %}
{{ radios(form.login_authentication) }}
{% endif %}
</div>
{% endif %}

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/radios.html" import radios %}
{% block per_page_title %}
Create an account
@@ -40,7 +41,7 @@ Create an account
{{ page_footer("Continue") }}
{{form.service}}
{{form.email_address}}
{{form.auth_type}}
{{ radios(form.auth_type) }}
{% endcall %}
</div>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/radios.html" import radios %}
{% block per_page_title %}
Create an account
@@ -26,7 +27,7 @@ Create an account
}) }}
</div>
{{ form.password(param_extensions={"hint": {"text": "At least 8 characters"}, "autocomplete": "new-password"}) }}
{{form.auth_type}}
{{ radios(form.auth_type) }}
{{ page_footer("Continue") }}
{% endcall %}
</div>

View File

@@ -3,6 +3,7 @@
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/radios.html" import radios %}
{% block service_page_title %}
Set data retention
@@ -16,7 +17,9 @@
{{ page_header('Set data retention') }}
{% call form_wrapper() %}
{{ form.notification_type }}
<div class="notification-type-radios">
{{ radios(form.notification_type) }}
</div>
{{ form.days_of_retention }}
{{ page_footer('Add') }}
{% endcall %}

View File

@@ -4,6 +4,7 @@
{% from "components/live-search.html" import live_search %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/radios.html" import radios %}
{% set page_title = "Link service to organization" %}
@@ -26,7 +27,9 @@
) }}
{% call form_wrapper(data_force_focus=True) %}
{% if has_organizations %}
{{ form.organizations }}
<div class="organizations-radios">
{{ radios(form.organizations) }}
</div>
{{ sticky_page_footer('Save') }}
{% else %}
<p class="hint"> No organizations </p>

View File

@@ -2,6 +2,7 @@
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/select/macro.njk" import usaSelect -%}
{% from "components/radios.html" import radios %}
{% block per_page_title %}
Set up your profile
@@ -68,7 +69,7 @@ Set up your profile
]
})
}}-->
{{form.auth_type}}
{{ radios(form.auth_type) }}
{{ page_footer("Save") }}
{% endcall %}
</div>

View File

@@ -1,3 +1,5 @@
{% from "components/checkboxes.html" import checkboxes %}
{% macro format_item_name(name, separators=True) -%}
{%- if name is string -%}
{{- name -}}

View File

@@ -27,6 +27,14 @@
}) }}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) and form.users_with_permission.all_service_users %}
{{ form.users_with_permission }}
{% if form.platform_admins is defined and form.platform_admins %}
<p class="usa-hint">Platform admin can see all folders:</p>
<ul class="usa-hint usa-list">
{% for admin in form.platform_admins %}
<li>{{ admin.name }}</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
{{ page_footer(

View File

@@ -2,6 +2,7 @@
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/radios.html" import radios %}
{% block service_page_title %}
{{ sender_context.title }}
@@ -15,20 +16,11 @@
<div class="grid-row">
{% call form_wrapper() %}
{{ form.sender(param_extensions={
'fieldset': {
'legend': {
'isPageHeading': True,
'text': sender_context.title
}
},
'classes': 'grid-col-9'
}) }}
{{ radios(form.sender, legend_style='usa-legend--large', option_hints=option_hints) }}
<div class="grid-col-9">
{{ page_footer('Continue') }}
</div>
{% endcall %}
</div>
</div>
{% endblock %}

View File

@@ -2,6 +2,7 @@
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/radios.html" import radios %}
{% set page_title = 'Set letter contact block' %}
@@ -17,19 +18,10 @@
<div class="grid-row">
{% call form_wrapper() %}
{{ form.sender(param_extensions={
'fieldset': {
'legend': {
'isPageHeading': True,
'text': page_title
}
},
'classes': 'grid-col-9'
}) }}
{{ radios(form.sender) }}
<div class="grid-col-9">
{{ page_footer('Continue') }}
<a class="usa-link" href="{{ url_for('.service_add_letter_contact', service_id=current_service.id, from_template=template_id) }}">Add new sender</a>
</div>
</div>
{% endcall %}
</div>

View File

@@ -1,6 +1,6 @@
import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import pytz
from flask import current_app, json
from flask_login import current_user
@@ -179,26 +179,72 @@ def generate_notifications_csv(**kwargs):
raise Exception("Should never reach here")
def convert_report_date_to_preferred_timezone(db_date_str_in_utc):
def convert_report_date_to_preferred_timezone(db_date_str_in_utc, target_timezone=None):
"""
Report dates in the db are in UTC. We need to convert them to the user's default timezone,
which defaults to "US/Eastern"
Format: "2025-05-14 07:22:18 AM America/Los_Angeles"
Args:
db_date_str_in_utc: UTC datetime string in format "YYYY-MM-DD HH:MM:SS"
target_timezone: Optional timezone name to use (e.g. "US/Eastern", "America/Los_Angeles")
If not provided, will use current user's preference or default
"""
date_arr = db_date_str_in_utc.split(" ")
db_date_str_in_utc = f"{date_arr[0]}T{date_arr[1]}+00:00"
utc_date_obj = datetime.datetime.fromisoformat(db_date_str_in_utc)
try:
date_arr = db_date_str_in_utc.split(" ")
db_date_str_in_utc = f"{date_arr[0]}T{date_arr[1]}+00:00"
utc_date_obj = datetime.datetime.fromisoformat(db_date_str_in_utc)
utc_date_obj = utc_date_obj.astimezone(pytz.utc)
preferred_timezone = pytz.timezone(get_user_preferred_timezone())
preferred_date_obj = utc_date_obj.astimezone(preferred_timezone)
preferred_tz_created_at = preferred_date_obj.strftime("%Y-%m-%d %I:%M:%S %p")
utc_date_obj = utc_date_obj.replace(tzinfo=ZoneInfo("UTC"))
return f"{preferred_tz_created_at} {get_user_preferred_timezone()}"
if not target_timezone:
target_timezone = get_user_preferred_timezone()
preferred_timezone = ZoneInfo(target_timezone)
preferred_date_obj = utc_date_obj.astimezone(preferred_timezone)
formatted_date = preferred_date_obj.strftime("%Y-%m-%d %I:%M:%S %p")
preferred_tz_created_at = f"{formatted_date} {target_timezone}"
return preferred_tz_created_at
except Exception as e:
try:
current_app.logger.error(
f"Error converting timezone for {db_date_str_in_utc}: {e}"
)
except RuntimeError:
pass
return f"{db_date_str_in_utc} UTC"
_timezone_cache = {}
def get_user_preferred_timezone():
if current_user and hasattr(current_user, "preferred_timezone"):
tz = current_user.preferred_timezone
if tz in pytz.all_timezones:
return tz
try:
if (
current_user
and current_user.is_authenticated
and hasattr(current_user, "preferred_timezone")
):
tz = current_user.preferred_timezone
if tz:
try:
ZoneInfo(tz)
return tz
except ZoneInfoNotFoundError:
pass
except (AttributeError, RuntimeError):
# AttributeError: current_user might not have expected attributes
# RuntimeError: working outside of request context
pass
return "US/Eastern"
def get_user_preferred_timezone_obj():
"""Get the ZoneInfo object for the user's preferred timezone, cached for performance."""
tz_name = get_user_preferred_timezone()
if tz_name not in _timezone_cache:
_timezone_cache[tz_name] = ZoneInfo(tz_name)
return _timezone_cache[tz_name]

63
app/utils/s3_csv.py Normal file
View File

@@ -0,0 +1,63 @@
import csv
import io
from app.utils.csv import (
convert_report_date_to_preferred_timezone,
get_user_preferred_timezone,
)
def convert_s3_csv_timestamps(csv_content, user_timezone=None):
"""
Convert UTC timestamps in CSV to user's preferred timezone.
Args:
csv_content: The CSV content as string or bytes
user_timezone: Optional pre-captured timezone (to avoid context issues in streaming)
"""
if isinstance(csv_content, bytes):
csv_content = csv_content.decode("utf-8")
if user_timezone is None:
user_timezone = get_user_preferred_timezone()
reader = csv.reader(io.StringIO(csv_content))
time_column_index = None
try:
header = next(reader)
for i, col in enumerate(header):
if col.strip().lower() == "time":
time_column_index = i
break
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(header)
yield output.getvalue()
output.truncate(0)
output.seek(0)
except StopIteration:
return
if time_column_index is None:
for row in reader:
writer.writerow(row)
yield output.getvalue()
output.truncate(0)
output.seek(0)
return
for row in reader:
if len(row) > time_column_index and row[time_column_index]:
try:
row[time_column_index] = convert_report_date_to_preferred_timezone(
row[time_column_index], target_timezone=user_timezone
)
except Exception: # nosec B110
pass
writer.writerow(row)
yield output.getvalue()
output.truncate(0)
output.seek(0)

View File

@@ -1,13 +1,13 @@
from datetime import datetime
from zoneinfo import ZoneInfo
import pytz
from dateutil import parser
from app.utils.csv import get_user_preferred_timezone
from app.utils.csv import get_user_preferred_timezone_obj
def get_current_financial_year():
preferred_tz = pytz.timezone(get_user_preferred_timezone())
preferred_tz = get_user_preferred_timezone_obj()
now = datetime.now(preferred_tz)
current_year = int(now.strftime("%Y"))
return current_year
@@ -15,7 +15,7 @@ def get_current_financial_year():
def is_less_than_days_ago(date_from_db, number_of_days):
return (
datetime.utcnow().astimezone(pytz.utc) - parser.parse(date_from_db)
datetime.now(ZoneInfo("UTC")) - parser.parse(date_from_db)
).days < number_of_days

View File

@@ -4,7 +4,10 @@ from app.enums import ServicePermission
permission_mappings = {
# TODO: consider turning off email-sending permissions during SMS pilot
ServicePermission.SEND_MESSAGES: [ServicePermission.SEND_TEXTS, ServicePermission.SEND_EMAILS],
ServicePermission.SEND_MESSAGES: [
ServicePermission.SEND_TEXTS,
ServicePermission.SEND_EMAILS,
],
ServicePermission.MANAGE_TEMPLATES: [ServicePermission.MANAGE_TEMPLATES],
ServicePermission.MANAGE_SERVICE: [
ServicePermission.MANAGE_USERS,

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 938 KiB

After

Width:  |  Height:  |  Size: 931 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

View File

@@ -1,7 +1,7 @@
env: demo
instances: 1
memory: 1G
command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application
command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py gunicorn_entry:application
public_admin_route: notify-demo.app.cloud.gov
cloud_dot_gov_route: notify-demo.app.cloud.gov
redis_enabled: 1

View File

@@ -1,7 +1,7 @@
env: production
instances: 2
memory: 2G
command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application
command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py gunicorn_entry:application
public_admin_route: beta.notify.gov
cloud_dot_gov_route: notify.app.cloud.gov
redis_enabled: 1

View File

@@ -1,7 +1,7 @@
env: sandbox
instances: 1
memory: 1G
command: gunicorn -c /home/vcap/app/gunicorn_config.py application
command: gunicorn -c /home/vcap/app/gunicorn_config.py gunicorn_entry:application
public_admin_route: notify-sandbox.app.cloud.gov
cloud_dot_gov_route: notify-sandbox.app.cloud.gov
redis_enabled: 1

View File

@@ -1,7 +1,7 @@
env: staging
instances: 1
memory: 1G
command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application
command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py gunicorn_entry:application
public_admin_route: notify-staging.app.cloud.gov
cloud_dot_gov_route: notify-staging.app.cloud.gov
redis_enabled: 1

View File

@@ -44,7 +44,6 @@ const javascripts = () => {
.pipe(buffer())
.pipe(
plugins.addSrc.prepend([
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
paths.npm + 'jquery/dist/jquery.min.js',
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'textarea-caret/index.js',
@@ -61,7 +60,6 @@ const javascripts = () => {
paths.src + 'javascripts/enhancedTextbox.js',
paths.src + 'javascripts/fileUpload.js',
paths.src + 'javascripts/radioSelect.js',
paths.src + 'javascripts/updateContent.js',
paths.src + 'javascripts/listEntry.js',
paths.src + 'javascripts/liveSearch.js',
paths.src + 'javascripts/errorTracking.js',
@@ -82,6 +80,7 @@ const javascripts = () => {
paths.src + 'javascripts/sidenav.js',
paths.src + 'javascripts/validation.js',
paths.src + 'javascripts/socketio.js',
paths.src + 'javascripts/scrollPosition.js',
])
.pipe(plugins.prettyerror())
.pipe(

View File

@@ -10,7 +10,9 @@ import gunicorn
# but we have an unusual configuration with a lot of cpus and not much memory
# so adjust it.
workers = multiprocessing.cpu_count()
worker_class = "eventlet"
worker_class = "gevent"
timeout = 240
bind = "0.0.0.0:{}".format(os.getenv("PORT"))
disable_redirect_access_to_syslog = True
gunicorn.SERVER_SOFTWARE = "None"

5
gunicorn_entry.py Normal file
View File

@@ -0,0 +1,5 @@
from gevent import monkey
monkey.patch_all()
from application import application # noqa

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