Production Release 12-8-25 (#3152)

* Update QA checklist

* Bump cachetools from 6.2.1 to 6.2.2 (#3099)

Bumps [cachetools](https://github.com/tkem/cachetools) from 6.2.1 to 6.2.2.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v6.2.1...v6.2.2)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-version: 6.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump moto from 5.1.16 to 5.1.17 (#3104)

Bumps [moto](https://github.com/getmoto/moto) from 5.1.16 to 5.1.17.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.16...5.1.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump numpy from 2.3.4 to 2.3.5 (#3103)

Bumps [numpy](https://github.com/numpy/numpy) from 2.3.4 to 2.3.5.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.4...v2.3.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump hypothesis from 6.147.0 to 6.148.1 (#3101)

Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.147.0 to 6.148.1.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.147.0...hypothesis-python-6.148.1)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump js-yaml (#3100)

Bumps  and [js-yaml](https://github.com/nodeca/js-yaml). These dependencies needed to be updated together.

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump click from 8.3.0 to 8.3.1 (#3102)

Bumps [click](https://github.com/pallets/click) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/click/compare/8.3.0...8.3.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump coverage from 7.11.3 to 7.12.0 (#3109)

Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.11.3 to 7.12.0.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](https://github.com/coveragepy/coveragepy/compare/7.11.3...7.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump hypothesis from 6.148.1 to 6.148.2 (#3108)

Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.1 to 6.148.2.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.1...hypothesis-python-6.148.2)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump webpack from 5.102.1 to 5.103.0 (#3107)

Bumps [webpack](https://github.com/webpack/webpack) from 5.102.1 to 5.103.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.102.1...v5.103.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.103.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump sass from 1.94.0 to 1.94.1 (#3106)

Bumps [sass](https://github.com/sass/dart-sass) from 1.94.0 to 1.94.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.94.0...1.94.1)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.94.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump bandit from 1.8.6 to 1.9.1 (#3105)

Bumps [bandit](https://github.com/PyCQA/bandit) from 1.8.6 to 1.9.1.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.8.6...1.9.1)

---
updated-dependencies:
- dependency-name: bandit
  dependency-version: 1.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* improve test coverage

* org test

* test coverage

* Bump sass from 1.94.1 to 1.94.2 (#3123)

Bumps [sass](https://github.com/sass/dart-sass) from 1.94.1 to 1.94.2.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.94.1...1.94.2)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.94.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump redis from 7.0.1 to 7.1.0 (#3122)

Bumps [redis](https://github.com/redis/redis-py) from 7.0.1 to 7.1.0.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v7.0.1...v7.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump phonenumbers from 9.0.18 to 9.0.19 (#3127)

Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.18 to 9.0.19.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.18...v9.0.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump exceptiongroup from 1.3.0 to 1.3.1 (#3128)

Bumps [exceptiongroup](https://github.com/agronholm/exceptiongroup) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/agronholm/exceptiongroup/releases)
- [Changelog](https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst)
- [Commits](https://github.com/agronholm/exceptiongroup/compare/1.3.0...1.3.1)

---
updated-dependencies:
- dependency-name: exceptiongroup
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Security update (#3129)

* Bump pytest-playwright from 0.7.1 to 0.7.2 (#3133)

Bumps [pytest-playwright](https://github.com/microsoft/playwright-pytest) from 0.7.1 to 0.7.2.
- [Release notes](https://github.com/microsoft/playwright-pytest/releases)
- [Commits](https://github.com/microsoft/playwright-pytest/compare/v0.7.1...v0.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump bandit from 1.9.1 to 1.9.2 (#3131)

Bumps [bandit](https://github.com/PyCQA/bandit) from 1.9.1 to 1.9.2.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.9.1...1.9.2)

---
updated-dependencies:
- dependency-name: bandit
  dependency-version: 1.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump pre-commit from 4.4.0 to 4.5.0 (#3132)

Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.4.0...v4.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump pytest from 8.4.2 to 9.0.1 (#3130)

Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.2 to 9.0.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump playwright from 1.56.1 to 1.57.0 (#3134)

Bumps [playwright](https://github.com/microsoft/playwright) from 1.56.1 to 1.57.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.56.1...v1.57.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump hypothesis from 6.148.2 to 6.148.3 (#3136)

Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.2 to 6.148.3.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.2...hypothesis-python-6.148.3)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump werkzeug from 3.1.3 to 3.1.4 (#3143)

Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-version: 3.1.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump hypothesis from 6.148.3 to 6.148.5 (#3138)

Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.3 to 6.148.5.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.3...hypothesis-python-6.148.5)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump flake8-bugbear from 25.10.21 to 25.11.29 (#3141)

Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 25.10.21 to 25.11.29.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/25.10.21...25.11.29)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-version: 25.11.29
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump beautifulsoup4 from 4.14.2 to 4.14.3 (#3137)

Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.14.2 to 4.14.3.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump hypothesis from 6.148.5 to 6.148.6 (#3145)

Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.5 to 6.148.6.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.5...hypothesis-python-6.148.6)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump urllib3 from 2.5.0 to 2.6.0 (#3146)

Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump phonenumbers from 9.0.19 to 9.0.20 (#3149)

Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 9.0.19 to 9.0.20.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v9.0.19...v9.0.20)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump hypothesis from 6.148.6 to 6.148.7 (#3148)

Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.148.6 to 6.148.7.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.148.6...hypothesis-python-6.148.7)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.148.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Remove SMS Total Message Limit from Org Dash (#3144)

* Remove SMS Total Message Limit from Org Dash

* update text

* pytest

* Fix dependabot prs (#3151)

* Bump terser-webpack-plugin from 5.3.14 to 5.3.15

Bumps [terser-webpack-plugin](https://github.com/webpack/terser-webpack-plugin) from 5.3.14 to 5.3.15.
- [Release notes](https://github.com/webpack/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack/terser-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/terser-webpack-plugin/compare/v5.3.14...v5.3.15)

---
updated-dependencies:
- dependency-name: terser-webpack-plugin
  dependency-version: 5.3.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump moto from 5.1.17 to 5.1.18

Bumps [moto](https://github.com/getmoto/moto) from 5.1.17 to 5.1.18.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.17...5.1.18)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Bump pip-audit from 2.9.0 to 2.10.0

Bumps [pip-audit](https://github.com/pypa/pip-audit) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/pypa/pip-audit/releases)
- [Changelog](https://github.com/pypa/pip-audit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pypa/pip-audit/compare/v2.9.0...v2.10.0)

---
updated-dependencies:
- dependency-name: pip-audit
  dependency-version: 2.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* 3110 dashboard design feedback & permission changes (#3142)

* update org dashboard

* allowing org admins to access serviecs within an org

* fix test

* Allow org admin to send messages within a service

* Bump coverage from 7.12.0 to 7.13.0 (#3159)

Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.12.0 to 7.13.0.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](https://github.com/coveragepy/coveragepy/compare/7.12.0...7.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump pytest from 9.0.1 to 9.0.2 (#3158)

Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.1 to 9.0.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.1...9.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump sass from 1.94.2 to 1.95.0 (#3157)

Bumps [sass](https://github.com/sass/dart-sass) from 1.94.2 to 1.95.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.94.2...1.95.0)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.95.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump black from 25.11.0 to 25.12.0 (#3155)

Bumps [black](https://github.com/psf/black) from 25.11.0 to 25.12.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.11.0...25.12.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump urllib3 from 2.6.0 to 2.6.1 (#3156)

Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.0...2.6.1)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Beverly Nguyen <Beverly.Nguyen@gsa.gov>
Co-authored-by: ccostino <ccostino@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Beverly Nguyen <53159604+heyitsmebev@users.noreply.github.com>
This commit is contained in:
Alex Janousek
2025-12-10 15:34:00 -05:00
committed by GitHub
parent 9f5fe219a8
commit bfaf36969f
29 changed files with 2742 additions and 3699 deletions

View File

@@ -123,7 +123,7 @@ def get_filtered_jobs(service_id, page):
@main.route("/activity/services/<uuid:service_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def all_jobs_activity(service_id):
service_data_retention_days = 8
page = get_page_from_request()

View File

@@ -23,7 +23,7 @@ dummy_bearer_token = "bearer_token_set" # nosec B105 - this is not a real token
@main.route("/services/<uuid:service_id>/api")
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_integration(service_id):
callbacks_link = (
".api_callbacks"
@@ -40,7 +40,7 @@ def api_integration(service_id):
@main.route("/services/<uuid:service_id>/api/documentation")
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_documentation(service_id):
return redirect(url_for(".documentation"), code=301)
@@ -51,7 +51,7 @@ def api_documentation(service_id):
endpoint="old_guest_list",
)
@main.route("/services/<uuid:service_id>/api/guest-list", methods=["GET", "POST"])
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def guest_list(service_id):
form = GuestList()
if form.validate_on_submit():
@@ -70,7 +70,7 @@ def guest_list(service_id):
@main.route("/services/<uuid:service_id>/api/keys")
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_keys(service_id):
return render_template(
"views/api/keys.html",
@@ -78,7 +78,7 @@ def api_keys(service_id):
@main.route("/services/<uuid:service_id>/api/keys/create", methods=["GET", "POST"])
@user_has_permissions("manage_api_keys", restrict_admin_usage=True)
@user_has_permissions("manage_api_keys", restrict_admin_usage=True, allow_org_user=True)
def create_api_key(service_id):
form = CreateKeyForm(current_service.api_keys)
form.key_type.choices = [
@@ -118,7 +118,7 @@ def create_api_key(service_id):
@main.route(
"/services/<uuid:service_id>/api/keys/revoke/<uuid:key_id>", methods=["GET", "POST"]
)
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def revoke_api_key(service_id, key_id):
key_name = current_service.get_api_key(key_id)["name"]
if request.method == "GET":
@@ -161,7 +161,7 @@ def check_token_against_dummy_bearer(token):
@main.route("/services/<uuid:service_id>/api/callbacks", methods=["GET"])
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def api_callbacks(service_id):
if not current_service.has_permission(ServicePermission.INBOUND_SMS):
return redirect(url_for(".delivery_status_callback", service_id=service_id))
@@ -192,7 +192,7 @@ def get_delivery_status_callback_details():
"/services/<uuid:service_id>/api/callbacks/delivery-status-callback",
methods=["GET", "POST"],
)
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def delivery_status_callback(service_id):
delivery_status_callback = get_delivery_status_callback_details()
back_link = (
@@ -259,7 +259,7 @@ def get_received_text_messages_callback():
"/services/<uuid:service_id>/api/callbacks/received-text-messages-callback",
methods=["GET", "POST"],
)
@user_has_permissions("manage_api_keys")
@user_has_permissions("manage_api_keys", allow_org_user=True)
def received_text_messages_callback(service_id):
if not current_service.has_permission(ServicePermission.INBOUND_SMS):
return redirect(url_for(".api_integration", service_id=service_id))

View File

@@ -13,7 +13,7 @@ from notifications_utils.template import SMSPreviewTemplate
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def conversation(service_id, notification_id):
user_number = get_user_number(service_id, notification_id)
@@ -31,7 +31,7 @@ def conversation(service_id, notification_id):
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>.json")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def conversation_updates(service_id, notification_id):
return jsonify(
get_conversation_partials(
@@ -46,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(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
def conversation_reply(
service_id,
notification_id,
@@ -70,7 +70,7 @@ def conversation_reply(
@main.route(
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/<uuid:template_id>"
)
@user_has_permissions(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
def conversation_reply_with_template(
service_id,
notification_id,

View File

@@ -24,13 +24,13 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/dashboard")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True)
def old_service_dashboard(service_id):
return redirect(url_for(".service_dashboard", service_id=service_id))
@main.route("/services/<uuid:service_id>")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def service_dashboard(service_id):
if session.get("invited_user_id"):
@@ -84,7 +84,7 @@ def job_is_finished(job_dict):
@main.route("/services/<uuid:service_id>/daily-stats.json")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def get_daily_stats(service_id):
date_range = get_stats_date_range()
days = date_range["days"]
@@ -148,7 +148,7 @@ def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
@main.route("/services/<uuid:service_id>/daily-stats-by-user.json")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def get_daily_stats_by_user(service_id):
date_range = get_stats_date_range()
days = date_range["days"]
@@ -166,7 +166,7 @@ def get_daily_stats_by_user(service_id):
@main.route("/services/<uuid:service_id>/template-usage")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def template_usage(service_id):
year, current_financial_year = requested_and_current_financial_year(request)
stats = template_statistics_client.get_monthly_template_usage_for_service(

View File

@@ -11,7 +11,7 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/history")
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def history(service_id):
events = _get_events(current_service.id, request.args.get("selected"))

View File

@@ -26,7 +26,7 @@ from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTem
@main.route("/services/<uuid:service_id>/jobs")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_jobs(service_id):
return redirect(
url_for(
@@ -37,7 +37,7 @@ def view_jobs(service_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_job(service_id, job_id):
job = Job.from_id(job_id, service_id=current_service.id)
if job.cancelled:
@@ -83,7 +83,7 @@ def view_job(service_id, job_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>.csv")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def view_job_csv(service_id, job_id):
job = Job.from_id(job_id, service_id=service_id)
filter_args = parse_filter_args(request.args)
@@ -111,14 +111,14 @@ def view_job_csv(service_id, job_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>", methods=["POST"])
@user_has_permissions(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
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))
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>/status.json")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_job_status_poll(service_id, job_id):
from app.notify_client.job_api_client import job_api_client
@@ -147,7 +147,7 @@ def view_job_status_poll(service_id, job_id):
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>/notifications-table")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_job_notifications_table(service_id, job_id):
"""Endpoint that returns only the notifications table HTML fragment."""
job = Job.from_id(job_id, service_id=current_service.id)

View File

@@ -44,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def invite_user(service_id, user_id=None):
form_class = InviteUserForm
form = form_class(
@@ -116,7 +116,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def edit_user_permissions(service_id, user_id):
service_has_email_auth = current_service.has_permission(
ServicePermission.EMAIL_AUTH
@@ -169,7 +169,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def remove_user_from_service(service_id, user_id):
try:
service_api_client.remove_user_from_service(service_id, user_id)
@@ -192,7 +192,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def edit_user_email(service_id, user_id):
user = current_service.get_team_member(user_id)
user_email = user.email_address
@@ -225,7 +225,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
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)
@@ -263,7 +263,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
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)
@@ -293,7 +293,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
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:
@@ -331,7 +331,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def cancel_invited_user(service_id, invited_user_id):
current_service.cancel_invite(invited_user_id)
@@ -350,7 +350,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def resend_invite(service_id, invited_user_id):
current_service.resend_invite(invited_user_id)

View File

@@ -35,7 +35,7 @@ from notifications_utils.s3 import S3ObjectNotFound
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True)
def view_notification(service_id, notification_id, error_message=None):
if error_message:
flash(error_message)
@@ -108,7 +108,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(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, ServicePermission.SEND_MESSAGES, allow_org_user=True)
def view_notification_updates(service_id, notification_id):
return jsonify(
**get_single_notification_partials(
@@ -149,7 +149,7 @@ PERIOD_TO_S3_FILENAME = {
@main.route("/services/<uuid:service_id>/download-notifications.csv")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def download_notifications_csv(service_id):
set_timezone()
filter_args = parse_filter_args(request.args)

View File

@@ -87,18 +87,14 @@ def add_organization():
return render_template("views/organizations/add-organization.html", form=form)
def get_organization_message_allowance(org_id):
def get_organization_messages_sent(org_id):
try:
message_usage = organizations_client.get_organization_message_usage(org_id)
except Exception as e:
current_app.logger.error(f"Error fetching organization message usage: {e}")
message_usage = {}
return {
"messages_sent": message_usage.get("messages_sent", 0),
"messages_remaining": message_usage.get("messages_remaining", 0),
"total_message_limit": message_usage.get("total_message_limit", 0),
}
return message_usage.get("messages_sent", 0)
def _handle_create_service(org_id):
@@ -210,9 +206,7 @@ def _handle_delete_service(org_id, service_id):
cached_service_user_ids = [user.id for user in service.active_users]
service_api_client.archive_service(service_id, cached_service_user_ids)
create_archive_service_event(
service_id=service_id, archived_by_id=current_user.id
)
create_archive_service_event(service_id=service_id, archived_by_id=current_user.id)
cache.redis_client.delete("organizations")
@@ -247,10 +241,10 @@ def get_services_dashboard_data(organization, year):
if sms_sent > 0 or sms_remainder > 0:
if sms_cost > 0:
usage_parts.append(
f"{sms_sent:,} sms ({sms_remainder:,} remaining, ${sms_cost:,.2f})"
f"{sms_sent:,} sms ({sms_remainder:,} message parts remaining, ${sms_cost:,.2f})"
)
else:
usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} remaining)")
usage_parts.append(f"{sms_sent:,} sms ({sms_remainder:,} message parts remaining)")
service["usage"] = ", ".join(usage_parts) if usage_parts else "No usage"
@@ -292,7 +286,7 @@ def organization_dashboard(org_id):
elif action == "delete-service" and service_id:
return _handle_delete_service(org_id, service_id)
message_allowance = get_organization_message_allowance(org_id)
messages_sent = get_organization_messages_sent(org_id)
return render_template(
"views/organizations/organization/index.html",
@@ -307,7 +301,7 @@ def organization_dashboard(org_id):
edit_service_data=edit_service_data,
new_service_id=session.pop("new_service_id", None),
updated_service_id=session.pop("updated_service_id", None),
**message_allowance,
messages_sent=messages_sent,
)

View File

@@ -95,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=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 +189,7 @@ def send_messages(service_id, template_id):
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>.csv", methods=["GET"])
@user_has_permissions(
ServicePermission.SEND_MESSAGES, ServicePermission.MANAGE_TEMPLATES
ServicePermission.SEND_MESSAGES, ServicePermission.MANAGE_TEMPLATES, allow_org_user=True
)
def get_example_csv(service_id, template_id):
template = get_template(
@@ -215,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def set_sender(service_id, template_id):
session["sender_id"] = None
redirect_to_one_off = redirect(
@@ -344,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_one_off(service_id, template_id):
session["recipient"] = None
session["placeholders"] = {}
@@ -388,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_one_off_step(service_id, template_id, step_index):
if {"recipient", "placeholders"} - set(session.keys()):
return redirect(
@@ -650,7 +650,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=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
@@ -694,7 +694,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def preview_job(service_id, template_id, upload_id, row_index=2):
session["scheduled_for"] = request.form.get("scheduled_for", "")
data = _check_messages(
@@ -709,7 +709,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def start_job(service_id, upload_id):
scheduled_for = session.pop("scheduled_for", None)
job_api_client.create_job(
@@ -846,7 +846,7 @@ 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(ServicePermission.SEND_MESSAGES)
and current_user.has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
):
return (
"Use my {}".format(first_column_headings[template.template_type][0]),
@@ -862,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_one_off_to_myself(service_id, template_id):
current_app.logger.info("Send one off to myself")
try:
@@ -899,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def check_notification(service_id, template_id):
return render_template(
"views/notifications/check.html",
@@ -981,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def preview_notification(service_id, template_id):
recipient = get_recipient()
if not recipient:
@@ -1013,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def send_notification(service_id, template_id):
recipient = get_recipient()

View File

@@ -70,7 +70,7 @@ PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
@main.route("/services/<uuid:service_id>/service-settings")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True)
def service_settings(service_id):
return render_template(
"views/service-settings.html",
@@ -81,7 +81,7 @@ def service_settings(service_id):
@main.route(
"/services/<uuid:service_id>/service-settings/name", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_name_change(service_id):
form = RenameServiceForm(name=current_service.name)
@@ -195,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def archive_service(service_id):
if not current_service.active or not (
current_service.trial_mode or current_user.platform_admin
@@ -268,7 +268,7 @@ def resume_service(service_id):
"/services/<uuid:service_id>/service-settings/send-files-by-email",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def send_files_by_email_contact_details(service_id):
form = ServiceContactDetailsForm()
contact_details = None
@@ -298,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_reply_to_email(service_id):
return redirect(url_for(".service_email_reply_to", service_id=service_id))
@@ -306,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(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True)
def service_email_reply_to(service_id):
return render_template("views/service-settings/email_reply_to.html")
@@ -315,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_add_email_reply_to(service_id):
form = ServiceReplyToEmailForm()
first_email_address = current_service.count_email_reply_to_addresses == 0
@@ -359,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_verify_reply_to_address(service_id, notification_id):
replace = request.args.get("replace", False)
is_default = request.args.get("is_default", False)
@@ -379,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_verify_reply_to_address_updates(service_id, notification_id):
return jsonify(
**get_service_verify_reply_to_address_partials(service_id, notification_id)
@@ -456,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
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(
@@ -525,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
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,
@@ -538,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_inbound_number(service_id):
available_inbound_numbers = (
inbound_number_client.get_available_inbound_sms_numbers()
@@ -571,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_sms_prefix(service_id):
form = SMSPrefixForm(enabled=current_service.prefix_sms)
@@ -590,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_international_sms(service_id):
form = ServiceOnOffSettingForm(
"Send text messages to international phone numbers",
@@ -611,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_inbound_sms(service_id):
return render_template(
"views/service-settings/set-inbound-sms.html",
@@ -622,7 +622,7 @@ def service_set_inbound_sms(service_id):
"/services/<uuid:service_id>/service-settings/set-<channel>",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_channel(service_id, channel):
if channel not in {"email", "sms"}:
abort(404)
@@ -647,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_auth_type(service_id):
return render_template(
"views/service-settings/set-auth-type.html",
@@ -655,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(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys", allow_org_user=True)
def service_sms_senders(service_id):
return render_template(
"views/service-settings/sms-senders.html",
@@ -666,7 +666,7 @@ def service_sms_senders(service_id):
"/services/<uuid:service_id>/service-settings/sms-sender/add",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_add_sms_sender(service_id):
form = ServiceSmsSenderForm()
first_sms_sender = current_service.count_sms_senders == 0
@@ -694,7 +694,7 @@ def service_add_sms_sender(service_id):
methods=["GET"],
endpoint="service_confirm_delete_sms_sender",
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
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"]
@@ -732,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(ServicePermission.MANAGE_SERVICE)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_delete_sms_sender(service_id, sms_sender_id):
service_api_client.delete_sms_sender(
service_id=current_service.id,

View File

@@ -40,7 +40,7 @@ form_objects = {
@main.route("/services/<uuid:service_id>/templates/<uuid:template_id>")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_template(service_id, template_id):
template = current_service.get_template(template_id)
template_folder = current_service.get_template_folder(template["folder"])
@@ -82,7 +82,7 @@ def view_template(service_id, template_id):
"/services/<uuid:service_id>/templates/<template_type:template_type>/folders/<uuid:template_folder_id>",
methods=["GET", "POST"],
)
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def choose_template(service_id, template_type="all", template_folder_id=None):
template_folder = current_service.get_template_folder(template_folder_id)
@@ -244,7 +244,7 @@ def _view_template_version(service_id, template_id, version):
@main.route(
"/services/<uuid:service_id>/templates/<uuid:template_id>/version/<int:version>"
)
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def view_template_version(service_id, template_id, version):
return render_template(
"views/templates/template_history.html",
@@ -291,7 +291,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def choose_template_to_copy(
service_id,
from_service=None,
@@ -323,7 +323,7 @@ def choose_template_to_copy(
"/services/<uuid:service_id>/templates/copy/<uuid:template_id>",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def copy_template(service_id, template_id):
from_service = request.args.get("from_service")
@@ -380,7 +380,7 @@ def _get_template_copy_name(template, existing_templates):
"<template_type:notification_type>/<return_to>/<uuid:template_id>"
)
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def action_blocked(service_id, notification_type, return_to, template_id=None):
back_link = {
"add_new_template": partial(
@@ -412,7 +412,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
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
@@ -462,7 +462,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
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
@@ -527,7 +527,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def add_service_template(service_id, template_type, template_folder_id=None):
if template_type not in current_service.available_template_types:
return redirect(
@@ -591,7 +591,7 @@ def abort_403_if_not_admin_user():
"/services/<uuid:service_id>/templates/<uuid:template_id>/edit",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def edit_service_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -679,7 +679,7 @@ def edit_service_template(service_id, template_id):
"/services/<uuid:service_id>/templates/count-<template_type:template_type>-length",
methods=["POST"],
)
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def count_content_length(service_id, template_type):
if template_type not in {"sms"}:
abort(404)
@@ -749,7 +749,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def delete_service_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -807,7 +807,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def confirm_redact_template(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -828,7 +828,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def redact_template(service_id, template_id):
service_api_client.redact_service_template(service_id, template_id)
@@ -847,7 +847,7 @@ def redact_template(service_id, template_id):
@main.route("/services/<uuid:service_id>/templates/<uuid:template_id>/versions")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
@user_has_permissions(ServicePermission.VIEW_ACTIVITY, allow_org_user=True)
def view_template_versions(service_id, template_id):
return render_template(
"views/templates/choose_history.html",
@@ -867,7 +867,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def set_template_sender(service_id, template_id):
template = current_service.get_template_with_user_permission_or_403(
template_id, current_user

View File

@@ -15,7 +15,7 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/tour/<uuid:template_id>")
@user_has_permissions(ServicePermission.SEND_MESSAGES)
@user_has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
def begin_tour(service_id, template_id):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -48,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=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
@@ -167,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(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True, allow_org_user=True)
def check_tour_notification(service_id, template_id):
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
@@ -224,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(ServicePermission.MANAGE_TEMPLATES)
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES, allow_org_user=True)
def go_to_dashboard_after_tour(service_id, example_template_id):
service_api_client.delete_service_template(service_id, example_template_id)

View File

@@ -11,7 +11,7 @@ MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
@main.route("/services/<uuid:service_id>/uploads")
@user_has_permissions()
@user_has_permissions(allow_org_user=True)
def uploads(service_id):
# No tests have been written, this has been quickly prepared for user research.
# It's also very like that a new view will be created to show uploads.

View File

@@ -4,7 +4,7 @@
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Dashboard</a></li>
{% endif %}
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('usage') }}" href="{{ url_for('.organization_usage', org_id=current_org.id) }}">Usage</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Org admins</a></li>
{% if current_user.platform_admin %}
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('settings') }}" href="{{ url_for('.organization_settings', org_id=current_org.id) }}">Settings</a></li>
<li class="usa-sidenav__item"><a class="usa-link {{ org_navigation.is_selected('trial-services') }}" href="{{ url_for('.organization_trial_mode_services', org_id=current_org.id) }}">Trial mode services</a></li>

View File

@@ -9,23 +9,21 @@
{{ page_header('Organization Dashboard', size='large') }}
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent|default(0) }}" data-messages-remaining="{{ messages_remaining|default(0) }}" data-total-message-limit="{{ total_message_limit|default(0) }}">
<div class="margin-bottom-3">
<div class="grid-row flex-align-center">
<h2 id="chartTitle" class="margin-right-1 margin-y-0">Overall {{ selected_year }} Total Message Allowance</h2>
<h2 id="chartTitle" class="margin-right-1 margin-y-0">Overall {{ selected_year }} Messages Sent</h2>
<button
type="button"
class="usa-tooltip usa-tooltip__information margin-right-0"
data-position="top"
title="Combined totals across all services in {{ current_org.name }}: pending, failed, or delivered"
title="Total messages sent across all services in {{ current_org.name }}"
>
<span class="usa-sr-only">More information</span>
<img src="{{ asset_url('img/usa-icons/info.svg') }}" alt="" class="usa-icon" role="presentation" />
</button>
</div>
<svg id="totalMessageChart"></svg>
<div id="message"></div>
<div class="font-sans-xl text-primary-darker margin-top-1">{{ "{:,}".format(messages_sent|default(0)) }} sent</div>
</div>
<div id="totalMessageTable" class="margin-bottom-3"></div>
<div class="grid-row margin-bottom-3 maxw-tablet">
<div class="grid-col-12 tablet:grid-col-6 margin-bottom-2 tablet:margin-bottom-0 tablet:padding-right-1">
<div class="usa-summary-box height-full" role="region">
@@ -39,7 +37,7 @@
<div class="text-ink font-body-2xs line-height-sans-3">
<span class="text-bold">{{ live_services }}</span> Live
<span class="margin-left-1 text-bold">{{ trial_services }}</span> Trial
<span class="margin-left-1 text-bold">{{ suspended_services }}</span> Suspended
<span class="margin-left-1 text-bold">{{ suspended_services }}</span> Archived
</div>
</div>
</div>
@@ -142,6 +140,9 @@
{% endif %}
</div>
</form>
<div class="border-top-1px border-base-lighter margin-top-3 padding-top-3">
<a href="{{ url_for('main.invite_user', service_id=edit_service_data.id) }}" class="usa-button usa-button--outline">Invite team member</a>
</div>
</div>
<div
@@ -238,7 +239,7 @@
</h3>
<div id="what-is-service-content" class="usa-accordion__content usa-prose" hidden>
<p>
When you join Notify, you're added to a service. This is your organization's workspace for sending text messages and emails. Within your service, you can:
Services are your organization's workspaces for sending text messages and emails. If you have multiple teams using Notify for different purposes, it is helpful to set up a separate Service for each team, with its own managers and templates. There is no limit to the number of Services you can create, and messages sent by all of your Services contribute to your overall messaging usage. Within each Service, you can:
</p>
<ol class="usa-list">
<li>Create and edit message templates</li>
@@ -246,7 +247,8 @@
<li>View message status and history</li>
<li>Manage team members and permissions</li>
<li>Track usage and delivery statistics</li>
<li>If you work for multiple organizations, you may belong to multiple services and can switch between them.</li>
<li>Service members can be added to multiple services and can switch between them</li>
<li>Organizational administrators can always see all Services</li>
</ol>
</div>
</div>
@@ -275,7 +277,7 @@
<td><a href="{{ url_for('main.service_dashboard', service_id=service.id) }}" class="usa-link">{{ service.name }}</a></td>
<td>
{% if not service.active %}
Suspended
Archived
{% elif service.restricted %}
Trial
{% else %}

View File

@@ -5,7 +5,7 @@
{% from "components/components/button/macro.njk" import usaButton %}
{% block org_page_title %}
Team members
Organization administrators
{% endblock %}
{% block maincolumn_content %}
@@ -20,7 +20,7 @@
{% endif %}
<h1 class="font-body-2xl">
Team members
Organization administrators
</h1>
{% if show_search_box %}
@@ -66,7 +66,7 @@
<div class="js-stick-at-bottom-when-scrolling">
{{ usaButton({
"element": "a",
"text": "Invite team member",
"text": "Invite an organization administrator",
"href": url_for('.invite_org_user', org_id=current_org.id),
"classes": "usa-button"
}) }}

View File

@@ -15,7 +15,7 @@
{% block maincolumn_content %}
{{ page_header("Invite a team member") }}
{{ page_header("Invite an organization administrator") }}
{% call form_wrapper() %}
{{ form.email_address(
param_extensions={
@@ -26,11 +26,11 @@
<div class="margin-bottom-3">
<p class="form-label">
{{ current_org.name }} team members can:
{{ current_org.name }} organization administrators can:
</p>
<ul class="list list-bullet">
<li>see usage and team members for each service</li>
<li>invite other team members</li>
<li>access and manage all services</li>
<li>invite or remove other organization administrators</li>
</ul>
</div>

View File

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

View File

@@ -73,9 +73,9 @@ def unicode_truncate(s, length):
def should_skip_template_page(db_template):
return (
current_user.has_permissions(ServicePermission.SEND_MESSAGES)
current_user.has_permissions(ServicePermission.SEND_MESSAGES, allow_org_user=True)
and not current_user.has_permissions(
ServicePermission.MANAGE_TEMPLATES, "manage_api_keys"
ServicePermission.MANAGE_TEMPLATES, "manage_api_keys", allow_org_user=True
)
and not db_template["archived"]
)