Compare commits

...

65 Commits

Author SHA1 Message Date
Beverly Nguyen
1bf55e7fa3 Replace send_messages and manage_service strings across application with ServicePermission.SEND_MESSAGES and ServicePermission.MANAGE_SERVICE types 2025-07-15 23:56:58 -07:00
ccostino
591231c084 Merge pull request #2751 from GSA/dependabot/npm_and_yarn/rollup-4.45.0
Bump rollup from 4.44.2 to 4.45.0
2025-07-15 10:27:18 -04:00
ccostino
a5753cb20a Merge pull request #2750 from GSA/dependabot/pip/freezegun-1.5.3
Bump freezegun from 1.5.2 to 1.5.3
2025-07-15 09:53:17 -04:00
dependabot[bot]
128ca06270 Bump freezegun from 1.5.2 to 1.5.3
Bumps [freezegun](https://github.com/spulec/freezegun) from 1.5.2 to 1.5.3.
- [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.2...1.5.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 12:52:15 +00:00
ccostino
710631312f Merge pull request #2749 from GSA/dependabot/pip/certifi-2025.7.14
Bump certifi from 2025.7.9 to 2025.7.14
2025-07-15 08:50:20 -04:00
dependabot[bot]
f5d5aa0bf9 Bump rollup from 4.44.2 to 4.45.0
Bumps [rollup](https://github.com/rollup/rollup) from 4.44.2 to 4.45.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.44.2...v4.45.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 04:29:54 +00:00
dependabot[bot]
6f31bc14dc Bump certifi from 2025.7.9 to 2025.7.14
---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2025.7.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 02:35:27 +00:00
Alex Janousek
f849799bc4 Merge pull request #2746 from GSA/migrate/govuk-list-heading
Migrate/govuk list and heading to uswds
2025-07-14 11:27:35 -03:00
Alex Janousek
f940d1fde6 Merge pull request #2748 from GSA/dependabot/npm_and_yarn/playwright-1.54.1
Bump playwright from 1.54.0 to 1.54.1
2025-07-14 08:54:12 -03:00
dependabot[bot]
ecc8057b30 Bump playwright from 1.54.0 to 1.54.1
Bumps [playwright](https://github.com/microsoft/playwright) from 1.54.0 to 1.54.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.0...v1.54.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-11 23:05:53 +00:00
alexjanousekGSA
3e0ff8d353 Removed unused paas-failwhale page 2025-07-11 16:56:58 -03:00
alexjanousekGSA
6b2d783b37 Removed govuk header css and migrated to uswds 2025-07-11 16:32:59 -03:00
Alex Janousek
9cc4bbb5ce Merge pull request #2743 from GSA/cleanup/govuk-uswds-checkbox
Cleanup/govuk uswds checkbox
2025-07-11 14:41:55 -03:00
alexjanousekGSA
405b47a02e Cleaned up styles, removed more govuk unused styles 2025-07-11 13:08:33 -03:00
alexjanousekGSA
53a3143a8f Renamed methods from govuk 2025-07-11 11:59:28 -03:00
Alex Janousek
3b1d9689b4 Merge pull request #2745 from GSA/dependabot/npm_and_yarn/playwright-1.54.0
Bump playwright from 1.53.2 to 1.54.0
2025-07-11 10:54:29 -03:00
alexjanousekGSA
793e51a3a2 Updated checkboxes to uswds 2025-07-11 10:32:00 -03:00
dependabot[bot]
9a2c6d1111 Bump playwright from 1.53.2 to 1.54.0
Bumps [playwright](https://github.com/microsoft/playwright) from 1.53.2 to 1.54.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.53.2...v1.54.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-10 23:34:08 +00:00
ccostino
0267a82687 Merge pull request #2742 from GSA/dependabot/pip/certifi-2025.7.9
Bump certifi from 2025.6.15 to 2025.7.9
2025-07-10 17:17:35 -04:00
alexjanousekGSA
41b925982e Added checkbox base line tests 2025-07-10 13:56:21 -03:00
dependabot[bot]
bce4671803 Bump certifi from 2025.6.15 to 2025.7.9
Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.6.15 to 2025.7.9.
- [Commits](https://github.com/certifi/python-certifi/compare/2025.06.15...2025.07.09)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-09 23:07:27 +00:00
Alex Janousek
0faa32c268 Merge pull request #2738 from GSA/cleanup/govuk-uswds-radio
Cleanup/govuk uswds radio
2025-07-09 19:29:40 -03:00
Beverly Nguyen
fc54cfd941 Merge pull request #2737 from GSA/2721-create-enums-and-replace-hardcoded-status-strings-with-enums
PART 1: Created and implemented enums throughout the codebase
2025-07-09 15:27:48 -07:00
alexjanousekGSA
51dcdca047 Migrate checkbox template from GOV.UK to USWDS patterns
- Replace usaRadioLabel with usaLabel for checkbox labels
- Update hint classes from govuk-checkboxes__hint to usa-checkbox__label-description
- Change conditional reveal classes from govuk-specific to USWDS display-none utility
- Update template comments to reflect USWDS migration
- Add checkbox-specific routes to BackstopJS configuration for testing

This establishes a baseline for checkbox component migration with expected
spacing differences between GOV.UK and USWDS implementations.
2025-07-09 18:48:15 -03:00
alexjanousekGSA
d1c423a112 Add BackstopJS reference images for radio button pages
These reference images correspond to the radio button page scenarios added
in the previous commit. They capture the post-migration USWDS radio styling.
2025-07-09 16:59:21 -03:00
alexjanousekGSA
ffc6d6c5b7 Add BackstopJS coverage for radio button pages 2025-07-09 16:48:52 -03:00
alexjanousekGSA
a691f5c0dd Migrate radio buttons from GOV.UK to USWDS
- Replace .multiple-choice with .usa-radio classes
- Update radio templates to use USWDS patterns
- Remove GOV.UK dependencies from radio components
- Convert nested lists to flat divs with USWDS margin utilities
- Update tests to use new radio classes
2025-07-09 16:40:34 -03:00
Alex Janousek
3f42dc343b Merge pull request #2741 from GSA/dependabot/pip/phonenumbers-9.0.9
Bump phonenumbers from 9.0.8 to 9.0.9
2025-07-09 13:52:18 -03:00
dependabot[bot]
823593893d Bump phonenumbers from 9.0.8 to 9.0.9
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.8 to 9.0.9.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.8...v9.0.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-08 23:08:12 +00:00
Alex Janousek
f07c196734 Merge pull request #2739 from GSA/dependabot/pip/moto-5.1.8
Bump moto from 5.1.6 to 5.1.8
2025-07-08 08:27:11 -04:00
Alex Janousek
ad824bbc1b Merge pull request #2740 from GSA/dependabot/pip/bandit-1.8.6
Bump bandit from 1.8.5 to 1.8.6
2025-07-08 08:26:51 -04:00
dependabot[bot]
ea5e1f6d8c Bump bandit from 1.8.5 to 1.8.6
---
updated-dependencies:
- dependency-name: bandit
  dependency-version: 1.8.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-08 03:32:15 +00:00
dependabot[bot]
cf6f134cc1 Bump moto from 5.1.6 to 5.1.8
---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-08 02:31:11 +00:00
Alex Janousek
5d1e1caf24 Merge pull request #2734 from GSA/dependabot/npm_and_yarn/jest-30.0.4
Bump jest from 30.0.3 to 30.0.4
2025-07-07 11:49:33 -04:00
Alex Janousek
cc30337d6c Merge pull request #2733 from GSA/dependabot/npm_and_yarn/jest-environment-jsdom-30.0.4
Bump jest-environment-jsdom from 30.0.2 to 30.0.4
2025-07-07 11:49:09 -04:00
Alex Janousek
377d24357f Merge pull request #2735 from GSA/dependabot/pip/coverage-7.9.2
Bump coverage from 7.9.1 to 7.9.2
2025-07-07 11:48:51 -04:00
Alex Janousek
bf75705175 Merge pull request #2736 from GSA/dependabot/npm_and_yarn/rollup-4.44.2
Bump rollup from 4.44.1 to 4.44.2
2025-07-07 11:48:33 -04:00
Beverly Nguyen
3130b1399c isort . 2025-07-04 17:52:03 -07:00
Beverly Nguyen
fa9e45fbcd .value removed 2025-07-04 17:42:20 -07:00
Beverly Nguyen
ee2f777904 remove healthcheck.py changes 2025-07-04 17:33:25 -07:00
Beverly Nguyen
9d9fa1c394 removed .value because we have StrEnum 2025-07-04 17:30:30 -07:00
Beverly Nguyen
8a41de8f07 Using StrEnum to drop .value 2025-07-04 17:22:21 -07:00
Beverly Nguyen
b73b4ac73e PART 1: Created and implemented enums throughout the codebase to replace hardcoded status strings, improving type safety and reducing the risk of typos. 2025-07-04 16:37:04 -07:00
dependabot[bot]
e69cd8267d Bump rollup from 4.44.1 to 4.44.2
Bumps [rollup](https://github.com/rollup/rollup) from 4.44.1 to 4.44.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.44.1...v4.44.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-04 23:15:23 +00:00
dependabot[bot]
2cec15f097 Bump coverage from 7.9.1 to 7.9.2
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.9.1 to 7.9.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.9.1...7.9.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 23:41:06 +00:00
dependabot[bot]
a0c87e0773 Bump jest from 30.0.3 to 30.0.4
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 30.0.3 to 30.0.4.
- [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.4/packages/jest)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 23:23:51 +00:00
dependabot[bot]
b13657373e Bump jest-environment-jsdom from 30.0.2 to 30.0.4
Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 30.0.2 to 30.0.4.
- [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.4/packages/jest-environment-jsdom)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 23:23:37 +00:00
Beverly Nguyen
232d76369f Merge pull request #2729 from GSA/convert-govuk-button-to-uswds
Convert govuk button to uswds button
2025-07-03 14:43:27 -07:00
Alex Janousek
ddaa68c1de Merge pull request #2730 from GSA/dependabot/npm_and_yarn/babel/core-7.28.0
Bump @babel/core from 7.27.7 to 7.28.0
2025-07-02 21:43:36 -04:00
alexjanousekGSA
84bbc328b4 Fix collapsible checkboxes test selectors
The tests were failing because the .usa-button selector was too broad
and was matching the 'Select all' button instead of the main
expand/collapse button. Updated selectors to use .selection-footer__button
which is more specific to the button being tested.
2025-07-02 21:40:05 -04:00
dependabot[bot]
2b72e5df60 Bump @babel/core from 7.27.7 to 7.28.0
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.27.7 to 7.28.0.
- [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.0/packages/babel-core)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 01:35:48 +00:00
Alex Janousek
47d388ba29 Merge pull request #2732 from GSA/dependabot/pip/cryptography-45.0.5
Bump cryptography from 45.0.4 to 45.0.5
2025-07-02 21:34:58 -04:00
Alex Janousek
f5501e364e Merge pull request #2731 from GSA/dependabot/npm_and_yarn/babel/preset-env-7.28.0
Bump @babel/preset-env from 7.27.2 to 7.28.0
2025-07-02 21:34:35 -04:00
alexjanousekGSA
3e2fe8be04 Updated and tested all buttons 2025-07-02 21:30:00 -04:00
dependabot[bot]
f968aece17 Bump cryptography from 45.0.4 to 45.0.5
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.4 to 45.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.4...45.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 23:49:04 +00:00
dependabot[bot]
7002dfaba7 Bump @babel/preset-env from 7.27.2 to 7.28.0
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.27.2 to 7.28.0.
- [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.0/packages/babel-preset-env)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 23:20:43 +00:00
alexjanousekGSA
bee667a5e9 Set baseline with backstop reference files 2025-07-02 16:10:07 -04:00
alexjanousekGSA
7447a0080c Removed unused code that is not fully implemented 2025-07-02 15:25:57 -04:00
Alex Janousek
ac226f5ad8 Merge pull request #2727 from GSA/feature/remove-govuk
Update govuk-link to usa-link
2025-07-02 12:31:26 -04:00
Alex Janousek
afbdb2aa9a Merge pull request #2728 from GSA/dependabot/pip/pytest-xdist-3.8.0
Bump pytest-xdist from 3.7.0 to 3.8.0
2025-07-02 11:14:44 -04:00
dependabot[bot]
4e31738fd8 Bump pytest-xdist from 3.7.0 to 3.8.0
Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: pytest-xdist
  dependency-version: 3.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 00:00:50 +00:00
Alex Janousek
25d76d1350 Merge pull request #2726 from GSA/dependabot/npm_and_yarn/playwright-1.53.2
Bump playwright from 1.53.1 to 1.53.2
2025-07-01 12:41:45 -04:00
alexjanousekGSA
3f58647dc9 Replace govuk link with usa-link and tested 2025-07-01 12:41:07 -04:00
alexjanousekGSA
07bdad4134 New backstop reference files before uswds component swap with govuk 2025-07-01 11:21:17 -04:00
dependabot[bot]
23ab1dd6c2 Bump playwright from 1.53.1 to 1.53.2
Bumps [playwright](https://github.com/microsoft/playwright) from 1.53.1 to 1.53.2.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.53.1...v1.53.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 02:30:53 +00:00
141 changed files with 1362 additions and 11518 deletions

View File

@@ -159,22 +159,6 @@ clean:
upload-static:
aws s3 cp --region us-west-2 --recursive --cache-control max-age=315360000,immutable ./app/static s3://${DNS_NAME}-static
# .PHONY: cf-failwhale-deployed
# cf-failwhale-deployed:
# @cf app notify-admin-failwhale --guid || (echo "notify-admin-failwhale is not deployed on ${CF_SPACE}" && exit 1)
# .PHONY: enable-failwhale
# enable-failwhale: cf-target cf-failwhale-deployed ## Enable the failwhale app and disable admin
# @cf map-route notify-admin-failwhale ${DNS_NAME} --hostname www
# @cf unmap-route notify-admin ${DNS_NAME} --hostname www
# @echo "Failwhale is enabled"
# .PHONY: disable-failwhale
# disable-failwhale: cf-target cf-failwhale-deployed ## Disable the failwhale app and enable admin
# @cf map-route notify-admin ${DNS_NAME} --hostname www
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
# @echo "Failwhale is disabled"
.PHONY: test-single
test-single: export NEW_RELIC_ENVIRONMENT=test
test-single: ## Run a single test file

View File

@@ -359,6 +359,11 @@ def init_app(application):
def _attach_current_user():
return {"current_user": current_user}
@application.context_processor
def _attach_enums():
from app.enums import ServicePermission
return {"ServicePermission": ServicePermission}
@application.context_processor
def _nav_selected():
return navigation

View File

@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 425.2 428" style="enable-background:new 0 0 425.2 428;" xml:space="preserve">
<style type="text/css">
.st0{fill:#1D70B8;}
.st1{fill:#0B0C0C;}
.st2{fill:#FFDD00;}
.st3{opacity:0.2;fill:#1D2F3A;}
.st4{opacity:0.5;fill:#F4F4F4;}
.st5{clip-path:url(#SVGID_2_);fill:#FFDD00;}
.st6{opacity:0.2;clip-path:url(#SVGID_2_);fill:#1D2F3A;}
.st7{opacity:0.5;clip-path:url(#SVGID_2_);}
.st8{fill:#F4F4F4;}
.st9{fill:#206FAB;}
</style>
<g id="Layer_1">
<circle class="st0" cx="212.6" cy="212.6" r="212.6"/>
</g>
<g id="Layer_2">
<rect x="91.9" y="248.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -164.6926 181.3616)" class="st1" width="89.3" height="81.7"/>
<rect x="76.9" y="258.5" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -150.6822 163.09)" class="st2" width="89.3" height="9.9"/>
<rect x="90.6" y="272.2" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -156.3567 176.7894)" class="st2" width="89.3" height="9.9"/>
<rect x="104.3" y="285.9" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -162.0312 190.4889)" class="st2" width="89.3" height="9.9"/>
<rect x="118" y="299.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -167.7057 204.1884)" class="st2" width="89.3" height="9.9"/>
<rect x="131.9" y="227.1" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -143.1309 190.2927)" class="st3" width="52.5" height="81.7"/>
</g>
<g id="Layer_5">
<polygon class="st4" points="116.8,275.2 108.5,283.4 103,274.9 111.3,266.7 "/>
<polygon class="st4" points="127.6,291.8 119.3,300 113.8,291.5 122.1,283.3 "/>
<polygon class="st4" points="138.3,308.4 130.1,316.7 124.6,308.2 132.8,299.9 "/>
<polygon class="st4" points="149.1,325 140.9,333.3 135.4,324.8 143.6,316.6 "/>
</g>
<g id="Layer_6">
</g>
<g id="Layer_4">
<defs>
<ellipse id="SVGID_1_" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st5" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st6" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
<path class="st5" d="M264.8,127.7c15.8,2.7,30.8,17.7,33.5,33.5s-7.9,26.4-23.7,23.7s-30.8-17.7-33.5-33.5
C238.4,135.6,249,125,264.8,127.7z"/>
<g class="st7">
<rect x="260.6" y="117.2" transform="matrix(0.842 -0.5395 0.5395 0.842 -44.6945 168.5803)" class="st8" width="9.8" height="86.7"/>
</g>
</g>
<g id="Layer_9">
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st9" cx="278.7" cy="147.3" rx="15.8" ry="15.8"/>
</g>
<g id="Layer_3">
<path class="st1" d="M278,74.5l-168,168l73.6,73.6l168-168L278,74.5z M262,164c-9.2-9.2-9.2-24.2,0-33.5s24.2-9.2,33.5,0
s9.2,24.2,0,33.5S271.3,173.3,262,164z"/>
</g>
<g id="Layer_8">
</g>
<g id="Layer_10">
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -65,7 +65,7 @@
return $(`<div class="selection-footer${stickyClass} margin-top-2">
<button
class="govuk-button govuk-button--secondary selection-footer__button usa-button usa-button--outline"
class="usa-button usa-button--outline selection-footer__button"
aria-expanded="${expanded ? 'true' : 'false'}"
aria-controls="${this.fieldsetId}">
${buttonContent}
@@ -214,7 +214,7 @@
CollapsibleCheckboxes.prototype.bindEvents = function() {
const self = this;
this.$formGroup.on('click', '.govuk-button', this.handleClick.bind(this));
this.$formGroup.on('click', '.usa-button', this.handleClick.bind(this));
this.$checkboxes.on('click', this.handleSelection.bind(this));
this.summary.bindEvents(this);

View File

@@ -80,14 +80,14 @@
$('.copy-to-clipboard__value', component)[0], () =>
$component
.html(states.valueCopied(stateOptions))
.find('.govuk-button').focus()
.find('.usa-button').focus()
)
)
.on(
'click', '.copy-to-clipboard__button--show', () =>
$component
.html(states.valueVisible(stateOptions))
.find('.govuk-button').focus()
.find('.usa-button').focus()
);
if ('stickAtBottomWhenScrolling' in GOVUK) {

View File

@@ -37,14 +37,14 @@
' {{{sharedAttributes}}}' +
'/>' +
'{{#button}}' +
'<button type="button" class="govuk-button govuk-button--secondary input-list__button--remove">' +
'<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="govuk-button govuk-button--secondary input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
'<button type="button" class="usa-button input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
);
ListEntry.prototype.getSharedAttributes = function () {
var $inputs = this.$wrapper.find('input'),

View File

@@ -108,9 +108,6 @@ p,
box-shadow: none !important;
}
.multiple-choice [type='radio']:focus + label::before {
box-shadow: 0 0 0 3px color('blue-40v');
}
.usa-dark-background .pill-item__container {
color: #fff;

View File

@@ -34,12 +34,6 @@ $path: '/static/images/';
// Dependencies from GOV.UK Frontend, packaged to be specific to this application
@import './govuk-frontend/all';
// Custom overrides
.govuk-link {
font-weight: bold;
color: #005ea5;
}
// Specific to this application
@import 'local/typography';
@import 'grids';

60
app/enums.py Normal file
View File

@@ -0,0 +1,60 @@
from enum import StrEnum
class NotificationStatus(StrEnum):
CREATED = "created"
PENDING = "pending"
SENDING = "sending"
DELIVERED = "delivered"
SENT = "sent"
FAILED = "failed"
TEMPORARY_FAILURE = "temporary-failure"
PERMANENT_FAILURE = "permanent-failure"
TECHNICAL_FAILURE = "technical-failure"
VALIDATION_FAILED = "validation-failed"
CANCELLED = "cancelled"
class ApiKeyType(StrEnum):
NORMAL = "normal"
TEAM = "team"
TEST = "test"
class JobStatus(StrEnum):
PENDING = "pending"
IN_PROGRESS = "in progress"
FINISHED = "finished"
SENDING_LIMITS_EXCEEDED = "sending limits exceeded"
SCHEDULED = "scheduled"
CANCELLED = "cancelled"
READY_TO_SEND = "ready to send"
SENT_TO_DVLA = "sent to dvla"
class ServicePermission(StrEnum):
SEND_MESSAGES = "send_messages"
MANAGE_SERVICE = "manage_service"
class InvitedUserStatus(StrEnum):
ACCEPTED = "accepted"
CANCELLED = "cancelled"
EXPIRED = "expired"
class InvitedOrgUserStatus(StrEnum):
ACCEPTED = "accepted"
CANCELLED = "cancelled"
class VerificationStatus(StrEnum):
PENDING = "pending"
SUCCESS = "success"
class AuthType(StrEnum):
EMAIL_AUTH = "email_auth"
SMS_AUTH = "sms_auth"

View File

@@ -655,7 +655,7 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
auth_type = HiddenField("auth_type", validators=[DataRequired()])
def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
def uswds_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
# error messages
error_message = None
if field.errors:
@@ -695,7 +695,7 @@ def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
) # nosec
def govuk_checkboxes_field_widget(
def uswds_checkboxes_field_widget(
self, field, wrap_in_collapsible=False, param_extensions=None, **kwargs
):
def _wrap_in_collapsible(field_label, checkboxes_string):
@@ -812,9 +812,9 @@ def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
) # nosec
class GovukCheckboxField(BooleanField):
class USWDSCheckboxField(BooleanField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukCheckboxField, self).__init__(
super(USWDSCheckboxField, self).__init__(
label, validators, false_values=None, **kwargs
)
self.param_extensions = param_extensions
@@ -824,17 +824,17 @@ class GovukCheckboxField(BooleanField):
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_checkbox_field_widget(
return uswds_checkbox_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
class GovukCheckboxesField(SelectMultipleField):
class USWDSCheckboxesField(SelectMultipleField):
render_as_list = False
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukCheckboxesField, self).__init__(label, validators, **kwargs)
super(USWDSCheckboxesField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
def get_item_from_option(self, option):
@@ -854,32 +854,32 @@ class GovukCheckboxesField(SelectMultipleField):
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_checkboxes_field_widget(
return uswds_checkboxes_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
# Wraps checkboxes rendering in HTML needed by the collapsible JS
class GovukCollapsibleCheckboxesField(GovukCheckboxesField):
class USWDSCollapsibleCheckboxesField(USWDSCheckboxesField):
def __init__(
self, label="", validators=None, field_label="", param_extensions=None, **kwargs
):
super(GovukCollapsibleCheckboxesField, self).__init__(
super(USWDSCollapsibleCheckboxesField, self).__init__(
label, validators, param_extensions, **kwargs
)
self.field_label = field_label
def widget(self, field, **kwargs):
return govuk_checkboxes_field_widget(
return uswds_checkboxes_field_widget(
self, field, wrap_in_collapsible=True, param_extensions=None, **kwargs
)
# GovukCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
# USWDSCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
# collapsible JS
# NestedFieldMixin puts the items into a tree hierarchy, pre-rendering the sub-trees of the top-level items
class GovukCollapsibleNestedCheckboxesField(
NestedFieldMixin, GovukCollapsibleCheckboxesField
class USWDSCollapsibleNestedCheckboxesField(
NestedFieldMixin, USWDSCollapsibleCheckboxesField
):
NONE_OPTION_VALUE = None
render_as_list = True
@@ -998,7 +998,7 @@ class BasePermissionsForm(StripWhitespaceForm):
for item in ([{"name": "Templates", "id": None}] + all_template_folders)
]
folder_permissions = GovukCollapsibleNestedCheckboxesField(
folder_permissions = USWDSCollapsibleNestedCheckboxesField(
"Folders this team member can see", field_label="folder"
)
@@ -1012,7 +1012,7 @@ class BasePermissionsForm(StripWhitespaceForm):
validators=[DataRequired()],
)
permissions_field = GovukCheckboxesField(
permissions_field = USWDSCheckboxesField(
"Permissions",
filters=[filter_by_permissions],
choices=[(value, label) for value, label in permission_options],
@@ -1422,7 +1422,7 @@ class ServiceContactDetailsForm(StripWhitespaceForm):
class ServiceReplyToEmailForm(StripWhitespaceForm):
email_address = email_address(label="Reply-to email address", gov_user=False)
is_default = GovukCheckboxField("Make this email address the default")
is_default = USWDSCheckboxField("Make this email address the default")
class ServiceSmsSenderForm(StripWhitespaceForm):
@@ -1436,11 +1436,11 @@ class ServiceSmsSenderForm(StripWhitespaceForm):
DoesNotStartWithDoubleZero(),
],
)
is_default = GovukCheckboxField("Make this text message sender the default")
is_default = USWDSCheckboxField("Make this text message sender the default")
class ServiceEditInboundNumberForm(StripWhitespaceForm):
is_default = GovukCheckboxField("Make this text message sender the default")
is_default = USWDSCheckboxField("Make this text message sender the default")
class AdminNotesForm(StripWhitespaceForm):
@@ -1528,7 +1528,7 @@ class GuestList(StripWhitespaceForm):
class DateFilterForm(StripWhitespaceForm):
start_date = GovukDateField("Start Date", [validators.optional()])
end_date = GovukDateField("End Date", [validators.optional()])
include_from_test_key = GovukCheckboxField("Include test keys")
include_from_test_key = USWDSCheckboxField("Include test keys")
class RequiredDateFilterForm(StripWhitespaceForm):
@@ -1732,7 +1732,7 @@ class TemplateFolderForm(StripWhitespaceForm):
(item.id, item.name) for item in all_service_users
]
users_with_permission = GovukCollapsibleCheckboxesField(
users_with_permission = USWDSCollapsibleCheckboxesField(
"Team members who can see this folder", field_label="team member"
)
name = GovukTextInputField(
@@ -1851,7 +1851,7 @@ class TemplateAndFoldersSelectionForm(Form):
return self.move_to_new_folder_name.data
return None
templates_and_folders = GovukCheckboxesField(
templates_and_folders = USWDSCheckboxesField(
"Choose templates or folders",
validators=[required_for_ops("move-to-new-folder", "move-to-existing-folder")],
choices=[], # added to keep order of arguments, added properly in __init__
@@ -1883,21 +1883,10 @@ class TemplateAndFoldersSelectionForm(Form):
class AdminClearCacheForm(StripWhitespaceForm):
model_type = GovukCheckboxesField(
model_type = USWDSCheckboxesField(
"What do you want to clear today",
)
def validate_model_type(self, field):
if not field.data:
raise ValidationError("Select at least one option")
class ChangeSecurityKeyNameForm(StripWhitespaceForm):
security_key_name = GovukTextInputField(
"Name of key",
validators=[
DataRequired(message="Cannot be empty"),
MustContainAlphanumericCharacters(),
Length(max=255, message="Name of key must be 255 characters or fewer"),
],
)

View File

@@ -1,6 +1,7 @@
from flask import abort, render_template, request, url_for
from app import current_service, job_api_client
from app.enums import NotificationStatus
from app.formatters import get_time_left
from app.main import main
from app.utils.pagination import (
@@ -78,10 +79,6 @@ def handle_pagination(jobs, service_id, page):
return prev_page, next_page, pagination
JOB_STATUS_DELIVERED = "delivered"
JOB_STATUS_FAILED = "failed"
def get_job_statistics(job, status):
statistics = job.get("statistics", [])
for stat in statistics:
@@ -109,8 +106,8 @@ def create_job_dict_entry(job):
"activity_time": activity_time,
"created_by": job.get("created_by"),
"template_name": job.get("template_name"),
"delivered_count": get_job_statistics(job, JOB_STATUS_DELIVERED),
"failed_count": get_job_statistics(job, JOB_STATUS_FAILED),
"delivered_count": get_job_statistics(job, NotificationStatus.DELIVERED),
"failed_count": get_job_statistics(job, NotificationStatus.FAILED),
}

View File

@@ -2,6 +2,7 @@ from flask import jsonify, redirect, render_template, session, url_for
from flask_login import current_user
from app import current_service, notification_api_client, service_api_client
from app.enums import ServicePermission
from app.main import main
from app.main.forms import SearchByNameForm
from app.models.template_list import TemplateList
@@ -45,7 +46,7 @@ def conversation_updates(service_id, notification_id):
@main.route(
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/from-folder/<uuid:from_folder>"
)
@user_has_permissions("send_messages")
@user_has_permissions(ServicePermission.SEND_MESSAGES)
def conversation_reply(
service_id,
notification_id,
@@ -69,7 +70,7 @@ def conversation_reply(
@main.route(
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/<uuid:template_id>"
)
@user_has_permissions("send_messages")
@user_has_permissions(ServicePermission.SEND_MESSAGES)
def conversation_reply_with_template(
service_id,
notification_id,

View File

@@ -14,6 +14,7 @@ from app import (
service_api_client,
template_statistics_client,
)
from app.enums import ServicePermission
from app.main import main
from app.main.views.user_profile import set_timezone
from app.statistics_utils import get_formatted_percentage
@@ -23,7 +24,7 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/dashboard")
@user_has_permissions("view_activity", "send_messages")
@user_has_permissions("view_activity", ServicePermission.SEND_MESSAGES)
def old_service_dashboard(service_id):
return redirect(url_for(".service_dashboard", service_id=service_id))
@@ -202,7 +203,7 @@ def template_usage(service_id):
@main.route("/services/<uuid:service_id>/usage")
@user_has_permissions("manage_service", allow_org_user=True)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def usage(service_id):
year, current_financial_year = requested_and_current_financial_year(request)

View File

@@ -4,13 +4,14 @@ from operator import attrgetter
from flask import render_template, request
from app import current_service, format_date_numeric
from app.enums import ServicePermission
from app.main import main
from app.models.event import APIKeyEvent, APIKeyEvents, ServiceEvents
from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/history")
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def history(service_id):
events = _get_events(current_service.id, request.args.get("selected"))

View File

@@ -2,6 +2,7 @@ from flask import abort, flash, redirect, render_template, session, url_for
from flask_login import current_user
from markupsafe import Markup
from app.enums import InvitedOrgUserStatus, InvitedUserStatus
from app.main import main
from app.models.organization import Organization
from app.models.service import Service
@@ -29,14 +30,14 @@ def accept_invite(token):
abort(403)
if invited_user.status == "cancelled":
if invited_user.status == InvitedUserStatus.CANCELLED:
service = Service.from_id(invited_user.service)
return render_template(
"views/cancelled-invitation.html",
from_user=invited_user.from_user.name,
service_name=service.name,
)
if invited_user.status == "accepted":
if invited_user.status == InvitedUserStatus.ACCEPTED:
session.pop("invited_user_id", None)
service = Service.from_id(invited_user.service)
return redirect(
@@ -104,7 +105,7 @@ def accept_org_invite(token):
abort(403)
if invited_org_user.status == "cancelled":
if invited_org_user.status == InvitedOrgUserStatus.CANCELLED:
organization = Organization.from_id(invited_org_user.organization)
return render_template(
"views/cancelled-invitation.html",
@@ -112,7 +113,7 @@ def accept_org_invite(token):
organization_name=organization.name,
)
if invited_org_user.status == "accepted":
if invited_org_user.status == InvitedOrgUserStatus.ACCEPTED:
session.pop("invited_org_user_id", None)
return redirect(
url_for("main.organization_dashboard", org_id=invited_org_user.organization)

View File

@@ -23,6 +23,7 @@ from app import (
notification_api_client,
service_api_client,
)
from app.enums import JobStatus, ServicePermission
from app.formatters import get_time_left, message_count_noun
from app.main import main
from app.main.forms import SearchNotificationsForm
@@ -105,7 +106,7 @@ def view_job_csv(service_id, job_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>", methods=["POST"])
@user_has_permissions("send_messages")
@user_has_permissions(ServicePermission.SEND_MESSAGES)
def cancel_job(service_id, job_id):
Job.from_id(job_id, service_id=service_id).cancel()
return redirect(url_for("main.service_dashboard", service_id=service_id))
@@ -398,7 +399,7 @@ def get_job_partials(job):
counts=_get_job_counts(job),
status=filter_args["status"],
notifications_deleted=(
job.status == "finished" and not notifications["notifications"]
job.status == JobStatus.FINISHED and not notifications["notifications"]
),
)
service_data_retention_days = current_service.get_days_of_retention(

View File

@@ -2,6 +2,7 @@ from flask import abort, flash, redirect, render_template, request, session, url
from flask_login import current_user
from app import current_service, service_api_client
from app.enums import ServicePermission
from app.event_handlers import (
create_cancel_user_invite_to_service_event,
create_email_change_event,
@@ -43,7 +44,7 @@ def manage_users(service_id):
@main.route(
"/services/<uuid:service_id>/users/invite/<uuid:user_id>", methods=["GET", "POST"]
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def invite_user(service_id, user_id=None):
form_class = InviteUserForm
form = form_class(
@@ -113,7 +114,7 @@ def invite_user(service_id, user_id=None):
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>", methods=["GET", "POST"])
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def edit_user_permissions(service_id, user_id):
service_has_email_auth = current_service.has_permission("email_auth")
user = current_service.get_team_member(user_id)
@@ -164,7 +165,7 @@ def edit_user_permissions(service_id, user_id):
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/delete", methods=["POST"])
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def remove_user_from_service(service_id, user_id):
try:
service_api_client.remove_user_from_service(service_id, user_id)
@@ -187,7 +188,7 @@ def remove_user_from_service(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def edit_user_email(service_id, user_id):
user = current_service.get_team_member(user_id)
user_email = user.email_address
@@ -220,7 +221,7 @@ def edit_user_email(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email/confirm",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def confirm_edit_user_email(service_id, user_id):
user = current_service.get_team_member(user_id)
session_key = "team_member_email_change-{}".format(user_id)
@@ -258,7 +259,7 @@ def confirm_edit_user_email(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def edit_user_mobile_number(service_id, user_id):
user = current_service.get_team_member(user_id)
user_mobile_number = redact_mobile_number(user.mobile_number)
@@ -288,7 +289,7 @@ def edit_user_mobile_number(service_id, user_id):
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number/confirm",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def confirm_edit_user_mobile_number(service_id, user_id):
user = current_service.get_team_member(user_id)
if "team_member_mobile_change" in session:
@@ -326,7 +327,7 @@ def confirm_edit_user_mobile_number(service_id, user_id):
"/services/<uuid:service_id>/cancel-invited-user/<uuid:invited_user_id>",
methods=["GET"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def cancel_invited_user(service_id, invited_user_id):
current_service.cancel_invite(invited_user_id)
@@ -345,7 +346,7 @@ def cancel_invited_user(service_id, invited_user_id):
"/services/<uuid:service_id>/resend-invite/<uuid:invited_user_id>",
methods=["GET"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def resend_invite(service_id, invited_user_id):
current_service.resend_invite(invited_user_id)

View File

@@ -12,6 +12,7 @@ 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.notify_client.api_key_api_client import KEY_TYPE_TEST
from app.utils import (
@@ -27,7 +28,7 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
@user_has_permissions("view_activity", "send_messages")
@user_has_permissions("view_activity", ServicePermission.SEND_MESSAGES)
def view_notification(service_id, notification_id, error_message=None):
if error_message:
flash(error_message)
@@ -100,7 +101,7 @@ def view_notification(service_id, notification_id, error_message=None):
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>.json")
@user_has_permissions("view_activity", "send_messages")
@user_has_permissions("view_activity", ServicePermission.SEND_MESSAGES)
def view_notification_updates(service_id, notification_id):
return jsonify(
**get_single_notification_partials(

View File

@@ -15,6 +15,7 @@ from flask import (
)
from app import redis_client, user_api_client
from app.enums import InvitedUserStatus
from app.main import main
from app.main.forms import (
RegisterUserFromOrgInviteForm,
@@ -254,14 +255,14 @@ def get_invited_user_email_address(invited_user_id):
def invited_user_accept_invite(invited_user_id):
invited_user = InvitedUser.by_id(invited_user_id)
if invited_user.status == "expired":
if invited_user.status == InvitedUserStatus.EXPIRED:
current_app.logger.error("User invitation has expired")
flash(
"Your invitation has expired; please contact the person who invited you for additional help."
)
abort(401)
if invited_user.status == "cancelled":
if invited_user.status == InvitedUserStatus.CANCELLED:
current_app.logger.error("User invitation has been cancelled")
flash(
"Your invitation is no longer valid; please contact the person who invited you for additional help."

View File

@@ -27,6 +27,7 @@ from app import (
notification_api_client,
service_api_client,
)
from app.enums import ServicePermission
from app.main import main
from app.main.forms import (
ChooseTimeForm,
@@ -94,7 +95,7 @@ def get_example_csv_rows(template, use_example_as_example=True, submitted_fields
@main.route(
"/services/<uuid:service_id>/send/<uuid:template_id>/csv", methods=["GET", "POST"]
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def send_messages(service_id, template_id):
notification_count = service_api_client.get_notification_count(service_id)
remaining_messages = current_service.message_limit - notification_count
@@ -189,7 +190,7 @@ def send_messages(service_id, template_id):
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>.csv", methods=["GET"])
@user_has_permissions("send_messages", "manage_templates")
@user_has_permissions(ServicePermission.SEND_MESSAGES, "manage_templates")
def get_example_csv(service_id, template_id):
template = get_template(
service_api_client.get_service_template(service_id, template_id)["data"],
@@ -214,7 +215,7 @@ def get_example_csv(service_id, template_id):
"/services/<uuid:service_id>/send/<uuid:template_id>/set-sender",
methods=["GET", "POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def set_sender(service_id, template_id):
session["sender_id"] = None
redirect_to_one_off = redirect(
@@ -343,7 +344,7 @@ def get_sender_details(service_id, template_type):
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>/one-off")
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def send_one_off(service_id, template_id):
session["recipient"] = None
session["placeholders"] = {}
@@ -387,7 +388,7 @@ def get_notification_check_endpoint(service_id, template):
"/services/<uuid:service_id>/send/<uuid:template_id>/one-off/step-<int:step_index>",
methods=["GET", "POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def send_one_off_step(service_id, template_id, step_index):
if {"recipient", "placeholders"} - set(session.keys()):
return redirect(
@@ -647,7 +648,7 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/row-<int:row_index>",
methods=["GET"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def check_messages(service_id, template_id, upload_id, row_index=2):
data = _check_messages(service_id, template_id, upload_id, row_index)
data["allowed_file_extensions"] = Spreadsheet.ALLOWED_FILE_EXTENSIONS
@@ -691,7 +692,7 @@ def check_messages(service_id, template_id, upload_id, row_index=2):
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/preview/row-<int:row_index>",
methods=["POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def preview_job(service_id, template_id, upload_id, row_index=2):
session["scheduled_for"] = request.form.get("scheduled_for", "")
data = _check_messages(
@@ -706,7 +707,7 @@ def preview_job(service_id, template_id, upload_id, row_index=2):
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def start_job(service_id, upload_id):
scheduled_for = session.pop("scheduled_for", None)
job_api_client.create_job(
@@ -843,7 +844,9 @@ def get_skip_link(step_index, template):
and step_index == 0
and template.template_type in ("sms", "email")
and not (template.template_type == "sms" and current_user.mobile_number is None)
and current_user.has_permissions("manage_templates", "manage_service")
and current_user.has_permissions(
"manage_templates", ServicePermission.MANAGE_SERVICE
)
):
return (
"Use my {}".format(first_column_headings[template.template_type][0]),
@@ -859,7 +862,7 @@ def get_skip_link(step_index, template):
"/services/<uuid:service_id>/template/<uuid:template_id>/one-off/send-to-myself",
methods=["GET"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def send_one_off_to_myself(service_id, template_id):
current_app.logger.info("Send one off to myself")
try:
@@ -896,7 +899,7 @@ def send_one_off_to_myself(service_id, template_id):
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
methods=["GET"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def check_notification(service_id, template_id):
return render_template(
"views/notifications/check.html",
@@ -978,7 +981,7 @@ def get_template_error_dict(exception):
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check/preview",
methods=["POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def preview_notification(service_id, template_id):
recipient = get_recipient()
if not recipient:
@@ -1010,7 +1013,7 @@ def preview_notification(service_id, template_id):
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
methods=["POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def send_notification(service_id, template_id):
recipient = get_recipient()

View File

@@ -21,6 +21,7 @@ from app import (
organizations_client,
service_api_client,
)
from app.enums import ServicePermission, VerificationStatus
from app.event_handlers import (
create_archive_service_event,
create_resume_service_event,
@@ -69,7 +70,7 @@ PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
@main.route("/services/<uuid:service_id>/service-settings")
@user_has_permissions("manage_service", "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
def service_settings(service_id):
return render_template(
"views/service-settings.html",
@@ -80,7 +81,7 @@ def service_settings(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/name", methods=["GET", "POST"]
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_name_change(service_id):
form = RenameServiceForm(name=current_service.name)
@@ -194,7 +195,7 @@ def service_set_permission(service_id, permission):
@main.route(
"/services/<uuid:service_id>/service-settings/archive", methods=["GET", "POST"]
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def archive_service(service_id):
if not current_service.active or not (
current_service.trial_mode or current_user.platform_admin
@@ -267,7 +268,7 @@ def resume_service(service_id):
"/services/<uuid:service_id>/service-settings/send-files-by-email",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def send_files_by_email_contact_details(service_id):
form = ServiceContactDetailsForm()
contact_details = None
@@ -297,7 +298,7 @@ def send_files_by_email_contact_details(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/set-reply-to-email", methods=["GET"]
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_set_reply_to_email(service_id):
return redirect(url_for(".service_email_reply_to", service_id=service_id))
@@ -305,7 +306,7 @@ def service_set_reply_to_email(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to", methods=["GET"]
)
@user_has_permissions("manage_service", "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
def service_email_reply_to(service_id):
return render_template("views/service-settings/email_reply_to.html")
@@ -314,7 +315,7 @@ def service_email_reply_to(service_id):
"/services/<uuid:service_id>/service-settings/email-reply-to/add",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_add_email_reply_to(service_id):
form = ServiceReplyToEmailForm()
first_email_address = current_service.count_email_reply_to_addresses == 0
@@ -358,7 +359,7 @@ def service_add_email_reply_to(service_id):
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_verify_reply_to_address(service_id, notification_id):
replace = request.args.get("replace", False)
is_default = request.args.get("is_default", False)
@@ -378,7 +379,7 @@ def service_verify_reply_to_address(service_id, notification_id):
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify.json"
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_verify_reply_to_address_updates(service_id, notification_id):
return jsonify(
**get_service_verify_reply_to_address_partials(service_id, notification_id)
@@ -397,10 +398,10 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id):
if replace:
existing = current_service.get_email_reply_to_address(replace)
existing_is_default = existing["is_default"]
verification_status = "pending"
verification_status = VerificationStatus.PENDING
is_default = True if (request.args.get("is_default", False) == "True") else False
if notification["status"] in DELIVERED_STATUSES:
verification_status = "success"
verification_status = VerificationStatus.SUCCESS
if notification["to"] not in [
i["email_address"] for i in current_service.email_reply_to_addresses
]:
@@ -441,7 +442,7 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id):
first_email_address=first_email_address,
replace=replace,
),
"stop": 0 if verification_status == "pending" else 1,
"stop": 0 if verification_status == VerificationStatus.PENDING else 1,
}
@@ -455,7 +456,7 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id):
methods=["GET"],
endpoint="service_confirm_delete_email_reply_to",
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_edit_email_reply_to(service_id, reply_to_email_id):
form = ServiceReplyToEmailForm()
reply_to_email_address = current_service.get_email_reply_to_address(
@@ -524,7 +525,7 @@ def service_edit_email_reply_to(service_id, reply_to_email_id):
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:reply_to_email_id>/delete",
methods=["POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_delete_email_reply_to(service_id, reply_to_email_id):
service_api_client.delete_reply_to_email_address(
service_id=current_service.id,
@@ -537,7 +538,7 @@ def service_delete_email_reply_to(service_id, reply_to_email_id):
"/services/<uuid:service_id>/service-settings/set-inbound-number",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_set_inbound_number(service_id):
available_inbound_numbers = (
inbound_number_client.get_available_inbound_sms_numbers()
@@ -570,7 +571,7 @@ def service_set_inbound_number(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/sms-prefix", methods=["GET", "POST"]
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_set_sms_prefix(service_id):
form = SMSPrefixForm(enabled=current_service.prefix_sms)
@@ -589,7 +590,7 @@ def service_set_sms_prefix(service_id):
"/services/<uuid:service_id>/service-settings/set-international-sms",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_set_international_sms(service_id):
form = ServiceOnOffSettingForm(
"Send text messages to international phone numbers",
@@ -610,7 +611,7 @@ def service_set_international_sms(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/set-inbound-sms", methods=["GET"]
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_set_inbound_sms(service_id):
return render_template(
"views/service-settings/set-inbound-sms.html",
@@ -621,7 +622,7 @@ def service_set_inbound_sms(service_id):
"/services/<uuid:service_id>/service-settings/set-<channel>",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_set_channel(service_id, channel):
if channel not in {"email", "sms"}:
abort(404)
@@ -646,7 +647,7 @@ def service_set_channel(service_id, channel):
@main.route(
"/services/<uuid:service_id>/service-settings/set-auth-type", methods=["GET"]
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_set_auth_type(service_id):
return render_template(
"views/service-settings/set-auth-type.html",
@@ -654,7 +655,7 @@ def service_set_auth_type(service_id):
@main.route("/services/<uuid:service_id>/service-settings/sms-sender", methods=["GET"])
@user_has_permissions("manage_service", "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
def service_sms_senders(service_id):
return render_template(
"views/service-settings/sms-senders.html",
@@ -665,7 +666,7 @@ def service_sms_senders(service_id):
"/services/<uuid:service_id>/service-settings/sms-sender/add",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_add_sms_sender(service_id):
form = ServiceSmsSenderForm()
first_sms_sender = current_service.count_sms_senders == 0
@@ -693,7 +694,7 @@ def service_add_sms_sender(service_id):
methods=["GET"],
endpoint="service_confirm_delete_sms_sender",
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_edit_sms_sender(service_id, sms_sender_id):
sms_sender = current_service.get_sms_sender(sms_sender_id)
is_inbound_number = sms_sender["inbound_number_id"]
@@ -731,7 +732,7 @@ def service_edit_sms_sender(service_id, sms_sender_id):
"/services/<uuid:service_id>/service-settings/sms-sender/<uuid:sms_sender_id>/delete",
methods=["POST"],
)
@user_has_permissions("manage_service")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
def service_delete_sms_sender(service_id, sms_sender_id):
service_api_client.delete_sms_sender(
service_id=current_service.id,

View File

@@ -12,6 +12,7 @@ from app import (
template_folder_api_client,
template_statistics_client,
)
from app.enums import ServicePermission
from app.formatters import character_count, message_count
from app.main import main
from app.main.forms import (
@@ -117,7 +118,7 @@ def choose_template(service_id, template_type="all", template_folder_id=None):
single_notification_channel = notification_channels[0]
if request.method == "POST" and templates_and_folders_form.validate_on_submit():
if not current_user.has_permissions("manage_templates"):
if not current_user.has_permissions(ServicePermission.MANAGE_TEMPLATES):
abort(403)
try:
return process_folder_management_form(
@@ -266,7 +267,7 @@ def _add_template_by_type(template_type, template_folder_id):
@main.route(
"/services/<uuid:service_id>/templates/copy/from-service/<uuid:from_service>/from-folder/<uuid:from_folder>"
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def choose_template_to_copy(
service_id,
from_service=None,
@@ -298,7 +299,7 @@ def choose_template_to_copy(
"/services/<uuid:service_id>/templates/copy/<uuid:template_id>",
methods=["GET", "POST"],
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def copy_template(service_id, template_id):
from_service = request.args.get("from_service")
@@ -355,7 +356,7 @@ def _get_template_copy_name(template, existing_templates):
"<template_type:notification_type>/<return_to>/<uuid:template_id>"
)
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def action_blocked(service_id, notification_type, return_to, template_id=None):
back_link = {
"add_new_template": partial(
@@ -387,7 +388,7 @@ def action_blocked(service_id, notification_type, return_to, template_id=None):
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/manage",
methods=["GET", "POST"],
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def manage_template_folder(service_id, template_folder_id):
template_folder = current_service.get_template_folder_with_user_permission_or_403(
template_folder_id, current_user
@@ -401,7 +402,7 @@ def manage_template_folder(service_id, template_folder_id):
)
if form.validate_on_submit():
if (
current_user.has_permissions("manage_service")
current_user.has_permissions(ServicePermission.MANAGE_SERVICE)
and form.users_with_permission.all_service_users
):
users_with_permission = form.users_with_permission.data + [current_user.id]
@@ -437,7 +438,7 @@ def manage_template_folder(service_id, template_folder_id):
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/delete",
methods=["GET", "POST"],
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def delete_template_folder(service_id, template_folder_id):
template_folder = current_service.get_template_folder_with_user_permission_or_403(
template_folder_id, current_user
@@ -502,7 +503,7 @@ def delete_template_folder(service_id, template_folder_id):
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/add-<template_type:template_type>",
methods=["GET", "POST"],
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def add_service_template(service_id, template_type, template_folder_id=None):
if template_type not in current_service.available_template_types:
return redirect(
@@ -566,7 +567,7 @@ def abort_403_if_not_admin_user():
"/services/<uuid:service_id>/templates/<uuid:template_id>/edit",
methods=["GET", "POST"],
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def edit_service_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -724,7 +725,7 @@ def _get_content_count_error_and_message_for_template(template):
"/services/<uuid:service_id>/templates/<uuid:template_id>/delete",
methods=["GET", "POST"],
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def delete_service_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -782,7 +783,7 @@ def delete_service_template(service_id, template_id):
@main.route(
"/services/<uuid:service_id>/templates/<uuid:template_id>/redact", methods=["GET"]
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def confirm_redact_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -803,7 +804,7 @@ def confirm_redact_template(service_id, template_id):
@main.route(
"/services/<uuid:service_id>/templates/<uuid:template_id>/redact", methods=["POST"]
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def redact_template(service_id, template_id):
service_api_client.redact_service_template(service_id, template_id)
@@ -842,7 +843,7 @@ def view_template_versions(service_id, template_id):
"/services/<uuid:service_id>/templates/<uuid:template_id>/set-template-sender",
methods=["GET", "POST"],
)
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def set_template_sender(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user

View File

@@ -1,6 +1,7 @@
from flask import abort, redirect, render_template, session
from app import current_service, current_user, service_api_client, url_for
from app.enums import ServicePermission
from app.main import main
from app.main.views.send import (
all_placeholders_in_session,
@@ -14,7 +15,7 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/tour/<uuid:template_id>")
@user_has_permissions("send_messages")
@user_has_permissions(ServicePermission.SEND_MESSAGES)
def begin_tour(service_id, template_id):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -47,7 +48,7 @@ def begin_tour(service_id, template_id):
"/services/<uuid:service_id>/tour/<uuid:template_id>/step-<int:step_index>",
methods=["GET", "POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def tour_step(service_id, template_id, step_index):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -166,7 +167,7 @@ def _get_tour_step_back_link(service_id, template_id, step_index):
@main.route(
"/services/<uuid:service_id>/tour/<uuid:template_id>/check", methods=["GET"]
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
def check_tour_notification(service_id, template_id):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -223,7 +224,7 @@ def check_tour_notification(service_id, template_id):
@main.route("/services/<uuid:service_id>/end-tour/<uuid:example_template_id>")
@user_has_permissions("manage_templates")
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
def go_to_dashboard_after_tour(service_id, example_template_id):
service_api_client.delete_service_template(service_id, example_template_id)

View File

@@ -1,5 +1,6 @@
from werkzeug.utils import cached_property
from app.enums import JobStatus, NotificationStatus
from app.models import JSONModel, ModelList, PaginatedModelList
from app.notify_client.job_api_client import job_api_client
from app.notify_client.notification_api_client import notification_api_client
@@ -32,11 +33,11 @@ class Job(JSONModel):
@property
def cancelled(self):
return self.status == "cancelled"
return self.status == JobStatus.CANCELLED
@property
def scheduled(self):
return self.status == "scheduled"
return self.status == JobStatus.SCHEDULED
@property
def scheduled_for(self):
@@ -61,16 +62,18 @@ class Job(JSONModel):
@property
def notifications_delivered(self):
return self._aggregate_statistics("delivered", "sent")
return self._aggregate_statistics(
NotificationStatus.DELIVERED, NotificationStatus.SENT
)
@property
def notifications_failed(self):
return self._aggregate_statistics(
"failed",
"technical-failure",
"temporary-failure",
"permanent-failure",
"cancelled",
NotificationStatus.FAILED,
NotificationStatus.TECHNICAL_FAILURE,
NotificationStatus.TEMPORARY_FAILURE,
NotificationStatus.PERMANENT_FAILURE,
NotificationStatus.CANCELLED,
)
@property
@@ -95,7 +98,7 @@ class Job(JSONModel):
@property
def still_processing(self):
return self.status != "finished" or self.percentage_complete < 100
return self.status != JobStatus.FINISHED or self.percentage_complete < 100
@cached_property
def finished_processing(self):

View File

@@ -1,6 +1,7 @@
from flask import abort, current_app
from werkzeug.utils import cached_property
from app.enums import ServicePermission
from app.models import JSONModel, SortByNameMixin
from app.models.job import ImmediateJobs, PaginatedJobs, PaginatedUploads, ScheduledJobs
from app.models.organization import Organization
@@ -175,7 +176,7 @@ class Service(JSONModel, SortByNameMixin):
[
user
for user in self.team_members
if user.has_permission_for_service(self.id, "manage_service")
if user.has_permission_for_service(self.id, ServicePermission.MANAGE_SERVICE)
]
)
> 1

View File

@@ -5,6 +5,7 @@ from flask import abort, current_app, request, session
from flask_login import AnonymousUserMixin, UserMixin, login_user, logout_user
from werkzeug.utils import cached_property
from app.enums import InvitedUserStatus
from app.event_handlers import (
create_add_user_to_service_event,
create_set_user_permissions_event,
@@ -566,7 +567,7 @@ class InvitedUser(JSONModel):
# current_app.logger.warning(
# f"Checking invited user {self.id} for permissions: {permissions}"
# )
if self.status == "cancelled":
if self.status == InvitedUserStatus.CANCELLED:
return False
return set(self.permissions) > set(permissions)
@@ -574,7 +575,7 @@ class InvitedUser(JSONModel):
# current_app.logger.warn(
# f"Checking invited user {self.id} for permission: {permission} on service {service_id}"
# )
if self.status == "cancelled":
if self.status == InvitedUserStatus.CANCELLED:
return False
return self.service == service_id and permission in self.permissions

View File

@@ -2,6 +2,8 @@ from itertools import chain
from flask import request
from app.enums import ServicePermission
class Navigation:
mapping = {}
@@ -74,7 +76,7 @@ class HeaderNavigation(Navigation):
"delete_service_template",
"edit_service_template",
"manage_template_folder",
"send_messages",
ServicePermission.SEND_MESSAGES,
"send_one_off",
"send_one_off_step",
"send_one_off_to_myself",
@@ -177,7 +179,7 @@ class MainNavigation(Navigation):
"delete_service_template",
"edit_service_template",
"manage_template_folder",
"send_messages",
ServicePermission.SEND_MESSAGES,
"send_one_off",
"send_one_off_step",
"send_one_off_to_myself",

View File

@@ -1,9 +1,10 @@
from app.enums import ApiKeyType
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
# must match key types in notifications-api/app/models.py
KEY_TYPE_NORMAL = "normal"
KEY_TYPE_TEAM = "team"
KEY_TYPE_TEST = "test"
KEY_TYPE_NORMAL = ApiKeyType.NORMAL
KEY_TYPE_TEAM = ApiKeyType.TEAM
KEY_TYPE_TEST = ApiKeyType.TEST
class ApiKeyApiClient(NotifyAdminAPIClient):

View File

@@ -5,6 +5,7 @@ from urllib.parse import unquote
from flask import current_app, request
from app import redis_client
from app.enums import InvitedUserStatus
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
from app.utils.user_permissions import (
all_ui_permissions,
@@ -94,7 +95,7 @@ class InviteApiClient(NotifyAdminAPIClient):
return self.get(url=f"/invite/service/check/{token}")["data"]
def cancel_invited_user(self, service_id, invited_user_id):
data = {"status": "cancelled"}
data = {"status": InvitedUserStatus.CANCELLED}
data = _attach_current_user(data)
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
@@ -131,7 +132,7 @@ class InviteApiClient(NotifyAdminAPIClient):
@cache.delete("service-{service_id}")
@cache.delete("user-{invited_user_id}")
def accept_invite(self, service_id, invited_user_id):
data = {"status": "accepted"}
data = {"status": InvitedUserStatus.ACCEPTED}
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)

View File

@@ -1,6 +1,7 @@
import datetime
from zoneinfo import ZoneInfo
from app.enums import JobStatus
from app.extensions import redis_client
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
from app.utils.csv import get_user_preferred_timezone
@@ -8,17 +9,17 @@ from app.utils.csv import get_user_preferred_timezone
class JobApiClient(NotifyAdminAPIClient):
JOB_STATUSES = {
"scheduled",
"pending",
"in progress",
"finished",
"cancelled",
"sending limits exceeded",
"ready to send",
"sent to dvla",
JobStatus.SCHEDULED,
JobStatus.PENDING,
JobStatus.IN_PROGRESS,
JobStatus.FINISHED,
JobStatus.CANCELLED,
JobStatus.SENDING_LIMITS_EXCEEDED,
JobStatus.READY_TO_SEND,
JobStatus.SENT_TO_DVLA,
}
SCHEDULED_JOB_STATUS = "scheduled"
CANCELLED_JOB_STATUS = "cancelled"
SCHEDULED_JOB_STATUS = JobStatus.SCHEDULED
CANCELLED_JOB_STATUS = JobStatus.CANCELLED
NON_CANCELLED_JOB_STATUSES = JOB_STATUSES - {CANCELLED_JOB_STATUS}
NON_SCHEDULED_JOB_STATUSES = JOB_STATUSES - {
SCHEDULED_JOB_STATUS,
@@ -57,7 +58,7 @@ class JobApiClient(NotifyAdminAPIClient):
job["original_file_name"],
)
for job in self.get_jobs(service_id, limit_days=0)["data"]
if job["job_status"] != "cancelled"
if job["job_status"] != JobStatus.CANCELLED
)
def get_page_of_jobs(self, service_id, *, page, statuses=None, limit_days=None):

View File

@@ -1,3 +1,4 @@
from app.enums import InvitedOrgUserStatus
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
@@ -33,7 +34,7 @@ class OrgInviteApiClient(NotifyAdminAPIClient):
return resp["data"]
def cancel_invited_user(self, org_id, invited_user_id):
data = {"status": "cancelled"}
data = {"status": InvitedOrgUserStatus.CANCELLED}
data = _attach_current_user(data)
self.post(
url="/organization/{0}/invite/{1}".format(org_id, invited_user_id),
@@ -41,7 +42,7 @@ class OrgInviteApiClient(NotifyAdminAPIClient):
)
def accept_invite(self, org_id, invited_user_id):
data = {"status": "accepted"}
data = {"status": InvitedOrgUserStatus.ACCEPTED}
self.post(
url="/organization/{0}/invite/{1}".format(org_id, invited_user_id),
data=data,

View File

@@ -49,7 +49,7 @@
visuallyHiddenText: params.errorMessage.visuallyHiddenText
}) | indent(2) | trim }}
{% endif %}
<div class="govuk-checkboxes {%- if params.classes %} {{ params.classes }}{% endif %}"
<div class="usa-checkbox-group {%- if params.classes %} {{ params.classes }}{% endif %}"
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}
{%- if isConditional %} data-module="checkboxes"{% endif -%}>
{% for item in params.items %}
@@ -60,8 +60,8 @@
{% set itemHintId = id + "-item-hint" if hasHint else "" %}
{% set itemDescribedBy = describedBy if not hasFieldset else "" %}
{% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %}
<div class="govuk-checkboxes__item">
<input class="govuk-checkboxes__input" id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ item.value }}"
<div class="usa-checkbox">
<input class="usa-checkbox__input" id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ item.value }}"
{{-" checked" if item.checked }}
{{-" disabled" if item.disabled }}
{%- if item.conditional %} data-aria-controls="{{ conditionalId }}"{% endif -%}
@@ -70,14 +70,14 @@
{{ usaLabel({
html: item.html,
text: item.text,
classes: 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label.classes),
classes: 'usa-checkbox__label' + (' ' + item.label.classes if item.label.classes),
attributes: item.label.attributes,
for: id
}) | indent(6) | trim }}
{%- if hasHint %}
{{ usaHint({
id: itemHintId,
classes: 'govuk-checkboxes__hint',
classes: 'usa-checkbox__label-description',
attributes: item.hint.attributes,
html: item.hint.html,
text: item.hint.text
@@ -85,7 +85,7 @@
{%- endif %}
</div>
{% if item.conditional %}
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
<div class="usa-checkbox__conditional{% if not item.checked %} display-none{% endif %}" id="{{ conditionalId }}">
{{ item.conditional.html | safe }}
</div>
{% endif %}

View File

@@ -12,7 +12,7 @@
{% endif %}
{% if current_service %}
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
{% set secondaryNavigation = [
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active": secondary_navigation.is_selected('settings')},
{"href": url_for('main.sign_out'), "text": "Sign out"}

View File

@@ -17,7 +17,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
{% set button_data = {"text": button_text} %}
{% if destructive %}{% set _ = button_data.update({"classes": "govuk-button--warning page-footer__button"}) %}{% endif %}
{% if destructive %}{% set _ = button_data.update({"classes": "usa-button usa-button--secondary page-footer__button"}) %}{% endif %}
{% if centered_button %}{% set _ = button_data.update({"classes": "page-footer__button--centred"}) %}{% endif %}
{% if button_name %}{% set _ = button_data.update({"name": button_name}) %}{% endif %}
{% if button_value %}{% set _ = button_data.update({"value": button_value}) %}{% endif %}

View File

@@ -40,7 +40,7 @@
<div class="radio-select" data-module="radio-select" data-categories="{{ field.categories|join(',') }}" data-show-now-as-default="{{ show_now_as_default|string|lower }}">
<div class="radio-select-column">
{% for option in field %}
<div class="multiple-choice">
<div class="usa-radio">
{{ option }}
<label for="{{ option.id }}">
{{ option.label.text }}

View File

@@ -9,18 +9,19 @@
{% endmacro %}
{% macro select_list(options, child_map, disable=[], option_hints={}, input="radio") %}
<ul>
{% macro select_list(options, child_map, disable=[], option_hints={}, input="radio", indent_level=0) %}
{% for option in options %}
{% if child_map[option.data] %}
{% call select_input(option, disable, option_hints, as_list_item=True, input=input) %}
{{ select_list(child_map[option.data], child_map, disable, option_hints, input=input) }}
{% endcall %}
<div class="{% if indent_level > 0 %}margin-left-{{ indent_level * 4 }}{% 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 %}
{{ select_input(option, disable, option_hints, as_list_item=True, input=input) }}
<div class="{% if indent_level > 0 %}margin-left-{{ indent_level * 4 }}{% endif %}">
{{ select_input(option, disable, option_hints, as_list_item=False, input=input) }}
</div>
{% endif %}
{% endfor %}
</ul>
{% endmacro %}
@@ -63,11 +64,7 @@
{% endmacro %}
{% macro select_input(option, disable=[], option_hints={}, data_target=None, as_list_item=False, input="radio") %}
{% if as_list_item %}
<li class="multiple-choice test-one" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
{% else %}
<div class="usa-radio" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
{% endif %}
<input
id="{{ option.id }}" class="usa-radio__input" name="{{ option.name }}" type="{{ input }}" value="{{ option.data }}"
{% if option.data in disable %}
@@ -80,17 +77,13 @@
<label class="usa-radio__label" for="{{ option.id }}">
{{ option.label.text }}
{% if option_hints[option.data] %}
<div class="block-label-hint">
<span class="usa-hint">
{{ option_hints[option.data] }}
</div>
</span>
{% endif %}
</label>
{% if caller %}
{{ caller() }}
{% endif %}
{% if as_list_item %}
</li>
{% else %}
</div>
{% endif %}
{% endmacro %}

View File

@@ -1,16 +1,16 @@
{% from "components/components/error-message/macro.njk" import usaErrorMessage -%}
{% from "components/components/fieldset/macro.njk" import govukFieldset %}
{% from "components/components/hint/macro.njk" import usaHint %}
{% from "components/components/radio-label/macro.njk" import usaRadioLabel %}
{% from "components/components/label/macro.njk" import usaLabel %}
{#- Copied from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/checkboxes/template.njk
Changes:
- `formGroup` option to control whether or not the checkboxes are wrapped with a `usa-form-group` class
- `classes` option added to `item` allow custom classes on the `.govuk-checkboxes__item` element
- `classes` option added to `item.hint` allow custom classes on the `.usa-hint` element (added to GOVUK Frontend in v3.5.0 - remove when we update)
- `asList` option added the root `params` object to allow setting of the `.govuk-checkboxes` and `.govuk-checkboxes__item` element types
- `children` option added to `item` allowing the sending in of prerendered child checkboxes (allowing the creation of tree structures through recursion) -#}
{#- Updated from GOV.UK components to USWDS patterns
Changed from original GOV.UK template:
- Uses USWDS checkbox classes (usa-checkbox, usa-checkbox__input, usa-checkbox__label)
- Conditional content uses display-none utility class instead of govuk-specific classes
- Label uses usaLabel macro instead of radio label
- Hint text uses usa-checkbox__label-description class
- Maintains all original functionality including conditional reveals and nested checkboxes -#}
{#- If an id 'prefix' is not passed, fall back to using the name attribute
instead. We need this for error messages and hints as well -#}
{% set idPrefix = params.idPrefix if params.idPrefix else params.name %}
@@ -92,17 +92,17 @@
{%- if item.conditional %} data-aria-controls="{{ conditionalId }}"{% endif -%}
{%- if itemDescribedBy %} aria-describedby="{{ itemDescribedBy }}"{% endif -%}
{%- for attribute, value in item.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
{{ usaRadioLabel({
{{ usaLabel({
html: item.html,
text: item.text,
classes: (' ' + item.label.classes if item.label.classes),
classes: 'usa-checkbox__label' + (' ' + item.label.classes if item.label.classes),
attributes: item.label.attributes,
for: id
}) | indent(6) | trim }}
{%- if hasHint %}
{{ usaHint({
id: itemHintId,
classes: 'govuk-checkboxes__hint' + (' ' + item.hint.classes if item.hint.classes),
classes: 'usa-checkbox__label-description' + (' ' + item.hint.classes if item.hint.classes),
attributes: item.hint.attributes,
html: item.hint.html,
text: item.hint.text
@@ -112,14 +112,14 @@
{{ item.children | safe }}
{%- endif %}
{% if params.asList and item.conditional %}
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
<div class="usa-checkbox__conditional{% if not item.checked %} display-none{% endif %}" id="{{ conditionalId }}">
{{ item.conditional.html | safe }}
</div>
{% endif %}
{%- if item.after %}{{ item.after }}{% endif -%}
</{{ groupItemElement }}>
{% if not params.asList and item.conditional %}
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
<div class="usa-checkbox__conditional{% if not item.checked %} display-none{% endif %}" id="{{ conditionalId }}">
{{ item.conditional.html | safe }}
</div>
{% endif %}

View File

@@ -61,7 +61,7 @@
"element": "a",
"text": "Create an API key",
"href": url_for('.create_api_key', service_id=current_service.id),
"classes": "govuk-button--secondary"
"classes": "usa-button"
}) }}
</div>

View File

@@ -27,7 +27,7 @@
'messages',
) }}
{% if current_user.has_permissions('send_messages') %}
{% if current_user.has_permissions(ServicePermission.SEND_MESSAGES) %}
<p class="sms-message-reply-link">
<a class="usa-link" href="{{ url_for('.conversation_reply', service_id=current_service.id, notification_id=notification_id) }}">Send a text message to this phone number</a>
</p>

View File

@@ -43,7 +43,7 @@ Dashboard
<div id="totalMessageTable" class="margin-0"></div>
{% include 'views/dashboard/activity-table.html' %}
{% if current_user.has_permissions('manage_service') %}{% endif %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}{% endif %}
{% include 'views/dashboard/most-used-templates.html' %}

View File

@@ -19,7 +19,7 @@
<h1 class="font-body-2xl margin-0">
Team members
</h1>
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
{{ usaButton({
"element": "a",
"text": "Invite a team member",
@@ -108,7 +108,7 @@
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
{% if user.status == 'pending' or user.status == 'expired' %}
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id) }}">
Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span>

View File

@@ -50,7 +50,7 @@
{{ ajax_block(partials, updates_url, 'status', finished=finished) }}
{% endif %}
{% if current_user.has_permissions('send_messages') and current_user.has_permissions('view_activity') and template.template_type == 'sms' and can_receive_inbound %}
{% if current_user.has_permissions(ServicePermission.SEND_MESSAGES) and current_user.has_permissions('view_activity') and template.template_type == 'sms' and can_receive_inbound %}
<p>
<a class="usa-link" href="{{ url_for('.conversation', service_id=current_service.id, notification_id=notification_id, _anchor='n{}'.format(notification_id)) }}">See all text messages sent to this phone number</a>
</p>

View File

@@ -34,7 +34,7 @@
"element": "a",
"text": "New organization",
"href": url_for('main.add_organization'),
"classes": "govuk-button--secondary"
"classes": "usa-button"
}) }}
</div>

View File

@@ -68,7 +68,7 @@
"element": "a",
"text": "Invite team member",
"href": url_for('.invite_org_user', org_id=current_org.id),
"classes": "govuk-button--secondary"
"classes": "usa-button"
}) }}
</div>

View File

@@ -15,7 +15,7 @@
</div>
</div>
<h2 class="govuk-heading-m">
<h2 class="font-body-lg">
Messages sent since May 2023
</h2>
@@ -71,7 +71,7 @@
</p>
</div>
<h2 class="govuk-heading-m">
<h2 class="font-body-lg">
Messages sent within 10 seconds
</h2>
<div class="grid-row">
@@ -106,7 +106,7 @@
</div>
</div>
<h2 class="govuk-heading-m">
<h2 class="font-body-lg">
Organizations using Notify
</h2>
<div class="grid-row bottom-gutter">

View File

@@ -30,7 +30,7 @@
{{ edit_field(
'Change',
url_for('.service_name_change', service_id=current_service.id),
permissions=['manage_service'],
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='service name',
)
}}
@@ -46,7 +46,7 @@
service_id=current_service.id,
channel='sms'
),
permissions=['manage_service'],
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='your settings for sending text messages',
)}} #}
{% endcall %}
@@ -65,7 +65,7 @@
{{ edit_field(
'Manage',
url_for('.service_sms_senders', service_id=current_service.id),
permissions=['manage_service','manage_api_keys'],
permissions=[ServicePermission.MANAGE_SERVICE,'manage_api_keys'],
suffix='text message senders',
)
}}
@@ -78,7 +78,7 @@
{{ edit_field(
'Change',
url_for('.service_set_sms_prefix', service_id=current_service.id),
permissions=['manage_service'],
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='your settings for starting text messages with service name',
)
}}
@@ -91,7 +91,7 @@
{{ edit_field(
'Change',
url_for('.service_set_international_sms', service_id=current_service.id),
permissions=['manage_service'],
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='your settings for sending international text messages',
)
}}
@@ -104,7 +104,7 @@
{{ edit_field(
'Change',
url_for('.service_set_inbound_sms', service_id=current_service.id),
permissions=['manage_service'],
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='your settings for receiving text messages',
)
}}
@@ -128,7 +128,7 @@
channel='email',
service_id=current_service.id
),
permissions=['manage_service'],
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='your settings for sending emails',
)}}
{% endcall %}
@@ -146,7 +146,7 @@
{{ edit_field(
'Manage',
url_for('.service_email_reply_to', service_id=current_service.id),
permissions=['manage_service','manage_api_keys'],
permissions=[ServicePermission.MANAGE_SERVICE,'manage_api_keys'],
suffix='reply-to email addresses',
)
}}
@@ -158,7 +158,7 @@
{{ edit_field(
'Manage',
url_for('.send_files_by_email_contact_details', service_id=current_service.id),
permissions=['manage_service'],
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='sending files by email',
)}}
{% endcall %}
@@ -178,7 +178,7 @@
</ul>
<p>
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
To remove these restrictions, you can send us a
<a class="usa-link" href="mailto:notify-support@gsa.gov">request to go live</a>.
{% else %}

View File

@@ -36,7 +36,7 @@
</h2>
</div>
<div class="grid-col-3">
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
<a class="usa-link user-list-edit-link" href="{{ url_for('.service_edit_email_reply_to', service_id =current_service.id, reply_to_email_id = item.id) }}">
Change<span class="usa-sr-only"> {{ item.email_address }}</span>
</a>
@@ -55,7 +55,7 @@
</div>
<div class="grid-row">
<div class="grid-col-12">
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
<div class="js-stick-at-bottom-when-scrolling">
{{ usaButton({
"element": "a",

View File

@@ -38,7 +38,7 @@
</h2>
</div>
<div class="grid-col">
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
<a class="usa-link user-list-edit-link" href="{{ url_for('.service_edit_sms_sender', service_id=current_service.id, sms_sender_id = item.id) }}">
Change<span class="usa-sr-only"> {{ item.sms_sender }}</span>
</a>
@@ -52,7 +52,7 @@
{% endfor %}
</div>
{% if current_user.has_permissions('manage_service') %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
<div class="grid-row bottom-gutter">
<div class="grid-col-12">
{{ usaButton({

View File

@@ -8,7 +8,7 @@
<p class="hint">
This template was deleted {{ template._template.updated_at|format_datetime_relative }}.
</p>
{% elif not current_user.has_permissions('send_messages', 'manage_api_keys', 'manage_templates', 'manage_service') or not user_has_template_permission %}
{% elif not current_user.has_permissions(ServicePermission.SEND_MESSAGES, 'manage_api_keys', 'manage_templates', ServicePermission.MANAGE_SERVICE) or not user_has_template_permission %}
<p class="top-gutter-1-3 {% if template.template_type != 'sms' %}bottom-gutter{% endif %}">
If you need to send this
{{ 1|message_count_label(template.template_type, suffix='') }}
@@ -17,7 +17,7 @@
{% else %}
<div class="grid-row margin-bottom-5">
<div class="usa-button-group">
{% if current_user.has_permissions('send_messages', restrict_admin_usage=True) %}
{% if current_user.has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) %}
<a href="{{ url_for(".set_sender", service_id=current_service.id, template_id=template.id) }}" class="usa-button usa-pill-separate-item">
Use this template
</a>

View File

@@ -25,7 +25,7 @@
{{ form.name(param_extensions={
"classes": ""
}) }}
{% if current_user.has_permissions("manage_service") and form.users_with_permission.all_service_users %}
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) and form.users_with_permission.all_service_users %}
{{ form.users_with_permission }}
{% endif %}

View File

@@ -1,71 +0,0 @@
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/components/button/macro.njk" import usaButton %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/webauthn-api-check.html" import webauthn_api_check %}
{% from "components/components/error-message/macro.njk" import usaErrorMessage %}
{% set page_title = 'Get your security key' %}
{% block extra_javascripts_before_body %}
{{ webauthn_api_check() }}
{% endblock %}
{% block per_page_title %}
{{ page_title }}
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.user_profile') }) }}
{% endblock %}
{% block maincolumn_content %}
{{ usaErrorMessage({
"classes": "banner-dangerous display-none",
"html": (
'Theres a problem with your security key' +
'<p>Check you have the right key and try again. ' +
'If this does not work, ' +
'<a class="usa-link" href=' + url_for('main.support') + ">contact us</a>." +
'</p>'
),
"attributes": {
"aria-live": "polite",
"tabindex": '-1'
}
}) }}
<div class="grid-row">
<div class="grid-col-6">
{{ page_header(page_title) }}
<p>
You need to have your security key to sign in.
</p>
{{ usaButton({
"element": "button",
"text": "Check security key",
"classes": "govuk-button--secondary webauthn__api-required",
"attributes": {
"data-module": "authenticate-security-key",
"data-csrf-token": csrf_token(),
}
}) }}
{{ usaErrorMessage({
"classes": "webauthn__api-missing",
"text": "Your browser does not support security keys. Try signing in to Notify using a different browser."
}) }}
{{ usaErrorMessage({
"classes": "webauthn__no-js",
"text": "JavaScript is not available for this page. Security keys need JavaScript to work."
}) }}
</div>
<div class="grid-col-3">
<img src="{{ asset_url('images/security-key.svg') }}" alt="" class="webauthn-illustration" width="149" height="150">
</div>
</div>
{% endblock %}

View File

@@ -1,34 +0,0 @@
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% set page_title = 'Manage ' + '' + security_key.name + '' %}
{% block per_page_title %}
{{ page_title }}
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.user_profile_security_keys') }) }}
{% endblock %}
{% block maincolumn_content %}
{{ page_header(page_title) }}
<div class="grid-row">
<div class="grid-col-9">
{% call form_wrapper(autocomplete=True) %}
{{ form.security_key_name }}
{{ page_footer(
'Save',
delete_link=url_for(
'.user_profile_confirm_delete_security_key', key_id=security_key.id),
delete_link_text='Delete'
) }}
{% endcall %}
</div>
</div>
{% endblock %}

View File

@@ -1,107 +0,0 @@
{% extends "base.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/components/button/macro.njk" import usaButton %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% from "components/table.html" import edit_field, mapping_table, row, field, row_heading %}
{% from "components/webauthn-api-check.html" import webauthn_api_check %}
{% from "components/components/error-message/macro.njk" import usaErrorMessage %}
{% set page_title = 'Security keys' %}
{% set credentials = current_user.webauthn_credentials %}
{% block extra_javascripts_before_body %}
{{ webauthn_api_check() }}
{% endblock %}
{% block per_page_title %}
{{ page_title }}
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.user_profile') }) }}
{% endblock %}
{% block maincolumn_content %}
{% set webauthn_button %}
{{ usaButton({
"element": "button",
"text": "Register a key",
"classes": "govuk-button--secondary webauthn__api-required",
"attributes": {
"data-module": "register-security-key",
"data-csrf-token": csrf_token(),
}
}) }}
{{ usaErrorMessage({
"classes": "webauthn__api-missing",
"text": "Your browser does not support security keys. Try signing in to Notify using a different browser."
}) }}
{{ usaErrorMessage({
"classes": "webauthn__no-js",
"text": "JavaScript is not available for this page. Security keys need JavaScript to work."
}) }}
{% endset %}
<div class="grid-row">
{{ usaErrorMessage({
"classes": "banner-dangerous display-none",
"html": (
'Theres a problem with your security key' +
'<p>Check you have the right key and try again. ' +
'If this does not work, ' +
'<a class="usa-link" href=' + url_for('main.support') + ">contact us</a>." +
'</p>'
),
"attributes": {
"aria-live": "polite",
"tabindex": '-1'
}
}) }}
{% if credentials %}
<div class="grid-col-10">
{{ page_header(page_title) }}
<div class="body-copy-table">
{% call mapping_table(
caption=page_title,
field_headings=['Security key details', 'Action'],
field_headings_visible=False,
caption_visible=False,
) %}
{% for credential in credentials %}
{% call row() %}
{% call field() %}
<div class="usa-body">{{ credential.name }}</div>
<div class="usa-body hint">Registered {{ credential.created_at|format_delta }}</div>
{% endcall %}
{{ edit_field('Manage', url_for('.user_profile_manage_security_key', key_id=credential.id)) }}
{% endcall %}
{% endfor %}
{% endcall %}
</div>
{{ webauthn_button }}
</div>
{% else %}
<div class="grid-col-6">
{{ page_header(page_title) }}
<p>
Security keys are an alternative way of signing in to Notify,
instead of getting a code in a text message
</p>
<p>
You can buy any key thats compatible with the WebAuthn
standard.
</p>
{{ webauthn_button }}
</div>
<div class="grid-col-3">
<img src="{{ asset_url('images/security-key.svg') }}" alt="" class="webauthn-illustration" width="149" height="150">
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -7,16 +7,21 @@ from ordered_set import OrderedSet
from werkzeug.datastructures import MultiDict
from werkzeug.routing import RequestRedirect
from app.enums import NotificationStatus, ServicePermission
from notifications_utils.field import Field
SENDING_STATUSES = ["created", "pending", "sending"]
DELIVERED_STATUSES = ["delivered", "sent"]
SENDING_STATUSES = [
NotificationStatus.CREATED,
NotificationStatus.PENDING,
NotificationStatus.SENDING,
]
DELIVERED_STATUSES = [NotificationStatus.DELIVERED, NotificationStatus.SENT]
FAILURE_STATUSES = [
"failed",
"temporary-failure",
"permanent-failure",
"technical-failure",
"validation-failed",
NotificationStatus.FAILED,
NotificationStatus.TEMPORARY_FAILURE,
NotificationStatus.PERMANENT_FAILURE,
NotificationStatus.TECHNICAL_FAILURE,
NotificationStatus.VALIDATION_FAILED,
]
REQUESTED_STATUSES = SENDING_STATUSES + DELIVERED_STATUSES + FAILURE_STATUSES
@@ -78,7 +83,7 @@ def unicode_truncate(s, length):
def should_skip_template_page(db_template):
return (
current_user.has_permissions("send_messages")
current_user.has_permissions(ServicePermission.SEND_MESSAGES)
and not current_user.has_permissions("manage_templates", "manage_api_keys")
and not db_template["archived"]
)

View File

@@ -1,10 +1,12 @@
from itertools import chain
from app.enums import ServicePermission
permission_mappings = {
# TODO: consider turning off email-sending permissions during SMS pilot
"send_messages": ["send_texts", "send_emails"],
ServicePermission.SEND_MESSAGES: ["send_texts", "send_emails"],
"manage_templates": ["manage_templates"],
"manage_service": ["manage_users", "manage_settings"],
ServicePermission.MANAGE_SERVICE: ["manage_users", "manage_settings"],
"manage_api_keys": ["manage_api_keys"],
"view_activity": ["view_activity"],
}
@@ -14,9 +16,9 @@ all_db_permissions = set(chain(*permission_mappings.values()))
permission_options = (
("view_activity", "See dashboard"),
("send_messages", "Send messages"),
(ServicePermission.SEND_MESSAGES, "Send messages"),
("manage_templates", "Add and edit templates"),
("manage_service", "Manage settings, team and usage"),
(ServicePermission.MANAGE_SERVICE, "Manage settings, team and usage"),
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 938 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 452 KiB

View File

@@ -403,7 +403,7 @@ class SMSPreviewTemplate(BaseSMSTemplate):
.then(nl2br)
.then(
autolink_urls,
classes="govuk-link govuk-link--no-visited-state",
classes="usa-link usa-link--external",
),
}
)

View File

@@ -1,26 +0,0 @@
### What is it?
This is a simple static error page to present to users in case of a (planned) downtime.
It is deployed as an individual app and remains dormant until a route is assigned to it.
### How do I use it?
It should already be deployed, but if not you can deploy it by running
cf push notify-admin-failwhale
To enable it you need to run
make <environment> enable-failwhale
and to disable it
make <environment> disable-failwhale
Where `<environment>` is any of
- preview
- staging
- production

View File

@@ -1,130 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Sorry, the service is unavailable Notify.gov
</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0b0c0c" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="shortcut icon" sizes="16x16 32x32 48x48"
href="/static_503/images/favicon.ico?de7abc5226925203ac10b0a4a94af949" type="image/x-icon" />
<link rel="mask-icon" href="/static_503/images/usa-mask-icon.svg?2afd125b1db96109388d4a0727312f3e" color="#0b0c0c">
<link rel="apple-touch-icon" sizes="180x180"
href="/static_503/images/apple-touch-icon.png?a0f7e1b728a42016b247dc54ee40d055">
<link rel="stylesheet" media="screen" href="/static_503/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="print" href="/static_503/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
<style>
.govuk-header__container {
border-color: #005EA5
}
</style>
<meta property="og:image" content="/static_503/images/govuk-opengraph-image.png">
</head>
<body class="govuk-template__body ">
<a href="#main-content" class="govuk-skip-link">Skip to main content</a>
<header class="govuk-header " role="banner" data-module="header">
<div class="govuk-header__container govuk-width-container">
<div class="govuk-header__logo">
<a href="/accounts-or-dashboard" class="govuk-header__link govuk-header__link--homepage">
<span class="govuk-header__logotype">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="33"
viewBox="0 0 7410 3900">
<rect width="7410" height="3900" fill="#b22234" />
<path d="M0,450H7410m0,600H0m0,600H7410m0,600H0m0,600H7410m0,600H0" stroke="#fff" stroke-width="300" />
<rect width="2964" height="2100" fill="#3c3b6e" />
<g fill="#fff">
<g id="s18">
<g id="s9">
<g id="s5">
<g id="s4">
<path id="s"
d="M247,90 317.534230,307.082039 132.873218,172.917961H361.126782L176.465770,307.082039z" />
<use xlink:href="#s" y="420" />
<use xlink:href="#s" y="840" />
<use xlink:href="#s" y="1260" />
</g>
<use xlink:href="#s" y="1680" />
</g>
<use xlink:href="#s4" x="247" y="210" />
</g>
<use xlink:href="#s9" x="494" />
</g>
<use xlink:href="#s18" x="988" />
<use xlink:href="#s9" x="1976" />
<use xlink:href="#s5" x="2470" />
</g>
</svg>
<span class="govuk-header__logotype-text">US</span>
</span>
<span class="govuk-header__product-name">Notify</span>
</a>
</div>
</div>
</header>
<div class="govuk-width-container">
<main class="govuk-main-wrapper" id="main-content" role="main">
<div class="grid-row">
<div class="grid-col-12">
<h1 class="govuk-heading-l">
Sorry, the service is unavailable
</h1>
<p class="usa-body">
Were performing some essential updates. Notify will be back shortly.
</p>
<p class="usa-body">
Please check the <a class="usa-link"
href="https://status.notifications.service.gov.uk/">status page</a> for updates.
</p>
</div>
</div>
</main>
</div>
<footer class="usa-footer" role="contentinfo">
<div>
<div class="usa-footer__meta">
<div class="usa-footer__meta-item usa-footer__meta-item--grow">
<h2 class="usa-sr-only">Support links</h2>
<div class="usa-footer__meta-custom">
Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions"
class="usa-link">Technology Transformation Services</a>
</div>
<svg role="presentation" focusable="false" class="usa-footer__licence-logo"
xmlns="http://www.w3.org/2000/svg" viewbox="0 0 483.2 195.7" height="17" width="41">
<path fill="currentColor"
d="M421.5 142.8V.1l-50.7 32.3v161.1h112.4v-50.7zm-122.3-9.6A47.12 47.12 0 0 1 221 97.8c0-26 21.1-47.1 47.1-47.1 16.7 0 31.4 8.7 39.7 21.8l42.7-27.2A97.63 97.63 0 0 0 268.1 0c-36.5 0-68.3 20.1-85.1 49.7A98 98 0 0 0 97.8 0C43.9 0 0 43.9 0 97.8s43.9 97.8 97.8 97.8c36.5 0 68.3-20.1 85.1-49.7a97.76 97.76 0 0 0 149.6 25.4l19.4 22.2h3v-87.8h-80l24.3 27.5zM97.8 145c-26 0-47.1-21.1-47.1-47.1s21.1-47.1 47.1-47.1 47.2 21 47.2 47S123.8 145 97.8 145" />
</svg>
<span class="usa-footer__licence-description">
All content is available under the
<a class="usa-link"
href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" rel="license">Open
Government Licence v3.0</a>, except where otherwise stated
</span>
</div>
<!-- <div class="usa-footer__meta-item">
<a class="usa-footer__link usa-footer__copyright-logo"
href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">©
US copyright</a>
</div> -->
</div>
</div>
</footer>
</body>
</html>

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