Files
notifications-admin/app/main/views/service_settings.py
Alex Janousek bfaf36969f 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>
2025-12-10 15:34:00 -05:00

934 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from collections import OrderedDict
from datetime import datetime
from flask import (
abort,
current_app,
flash,
jsonify,
redirect,
render_template,
request,
url_for,
)
from flask_login import current_user
from app import (
billing_api_client,
current_service,
inbound_number_client,
notification_api_client,
organizations_client,
service_api_client,
)
from app.enums import ServicePermission, VerificationStatus
from app.event_handlers import (
create_archive_service_event,
create_resume_service_event,
create_suspend_service_event,
)
from app.formatters import email_safe
from app.main import main
from app.main.forms import (
AdminBillingDetailsForm,
AdminNotesForm,
AdminServiceAddDataRetentionForm,
AdminServiceEditDataRetentionForm,
AdminServiceInboundNumberForm,
AdminServiceMessageLimitForm,
AdminServiceRateLimitForm,
AdminServiceSMSAllowanceForm,
AdminSetOrganizationForm,
RenameServiceForm,
SearchByNameForm,
ServiceContactDetailsForm,
ServiceEditInboundNumberForm,
ServiceOnOffSettingForm,
ServiceReplyToEmailForm,
ServiceSmsSenderForm,
ServiceSwitchChannelForm,
SMSPrefixForm,
)
from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, SENDING_STATUSES
from app.utils.time import parse_naive_dt
from app.utils.user import user_has_permissions, user_is_platform_admin
from notifications_python_client.errors import HTTPError
PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
[
(
ServicePermission.INBOUND_SMS,
{
"title": "Receive inbound SMS",
"requires": "sms",
"endpoint": ".service_set_inbound_number",
},
),
(ServicePermission.EMAIL_AUTH, {"title": "Email authentication"}),
]
)
@main.route("/services/<uuid:service_id>/service-settings")
@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",
service_permissions=PLATFORM_ADMIN_SERVICE_PERMISSIONS,
)
@main.route(
"/services/<uuid:service_id>/service-settings/name", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_name_change(service_id):
form = RenameServiceForm(name=current_service.name)
if form.validate_on_submit():
try:
current_service.update(
name=form.name.data,
email_from=email_safe(form.name.data),
)
except HTTPError as http_error:
if http_error.status_code == 400 and any(
name_error_message.startswith("Duplicate service name")
for name_error_message in http_error.message["name"]
):
form.name.errors.append("This service name is already in use")
else:
raise http_error
else:
return redirect(url_for(".service_settings", service_id=service_id))
if current_service.organization_type == "local":
return render_template(
"views/service-settings/name-local.html",
form=form,
)
return render_template(
"views/service-settings/name.html",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/switch-live", methods=["GET", "POST"]
)
@user_is_platform_admin
def service_switch_live(service_id):
form = ServiceOnOffSettingForm(
name="Make service live", enabled=not current_service.trial_mode
)
if form.validate_on_submit():
current_service.update_status(live=form.enabled.data)
if form.enabled.data is True:
billing_api_client.create_or_update_free_sms_fragment_limit(
service_id, 250000
)
else:
billing_api_client.create_or_update_free_sms_fragment_limit(
service_id, 40000
)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-service-setting.html",
title="Make service live",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/switch-count-as-live",
methods=["GET", "POST"],
)
@user_is_platform_admin
def service_switch_count_as_live(service_id):
form = ServiceOnOffSettingForm(
name="Count as live",
enabled=current_service.count_as_live,
truthy="Yes",
falsey="No",
)
if form.validate_on_submit():
current_service.update_count_as_live(form.enabled.data)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-service-setting.html",
title="Count in list of live services",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/permissions/<permission>",
methods=["GET", "POST"],
)
@user_is_platform_admin
def service_set_permission(service_id, permission):
if permission not in PLATFORM_ADMIN_SERVICE_PERMISSIONS:
abort(404)
title = PLATFORM_ADMIN_SERVICE_PERMISSIONS[permission]["title"]
form = ServiceOnOffSettingForm(
name=title, enabled=current_service.has_permission(permission)
)
if form.validate_on_submit():
current_service.force_permission(permission, on=form.enabled.data)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-service-setting.html",
title=title,
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/archive", methods=["GET", "POST"]
)
@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
):
abort(403)
if request.method == "POST":
# We need to purge the cache for the services users as otherwise, although they will have had their permissions
# removed in the DB, they would still have permissions in the cache to view/edit/manage this service
cached_service_user_ids = [user.id for user in current_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
)
flash(
"{} was deleted".format(current_service.name),
"default_with_tick",
)
return redirect(url_for(".choose_account"))
else:
flash(
"Are you sure you want to delete {}? Theres no way to undo this.".format(
current_service.name
),
"delete",
)
return service_settings(service_id)
@main.route(
"/services/<uuid:service_id>/service-settings/suspend", methods=["GET", "POST"]
)
@user_is_platform_admin
def suspend_service(service_id):
if request.method == "POST":
service_api_client.suspend_service(service_id)
create_suspend_service_event(
service_id=service_id, suspended_by_id=current_user.id
)
return redirect(url_for(".service_settings", service_id=service_id))
else:
flash(
"This will suspend the service and revoke all api keys. Are you sure you want to suspend this service?",
"suspend",
)
return service_settings(service_id)
@main.route(
"/services/<uuid:service_id>/service-settings/resume", methods=["GET", "POST"]
)
@user_is_platform_admin
def resume_service(service_id):
if request.method == "POST":
service_api_client.resume_service(service_id)
create_resume_service_event(
service_id=service_id, resumed_by_id=current_user.id
)
return redirect(url_for(".service_settings", service_id=service_id))
else:
flash(
"This will resume the service. New api key are required for this service to use the API.",
"resume",
)
return service_settings(service_id)
@main.route(
"/services/<uuid:service_id>/service-settings/send-files-by-email",
methods=["GET", "POST"],
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def send_files_by_email_contact_details(service_id):
form = ServiceContactDetailsForm()
contact_details = None
if request.method == "GET":
contact_details = current_service.contact_link
if contact_details:
contact_type = check_contact_details_type(contact_details)
field_to_update = getattr(form, contact_type)
form.contact_details_type.data = contact_type
field_to_update.data = contact_details
if form.validate_on_submit():
contact_type = form.contact_details_type.data
current_service.update(contact_link=form.data[contact_type])
return redirect(url_for(".service_settings", service_id=current_service.id))
return render_template(
"views/service-settings/send-files-by-email.html",
form=form,
contact_details=contact_details,
)
@main.route(
"/services/<uuid:service_id>/service-settings/set-reply-to-email", methods=["GET"]
)
@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))
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to", methods=["GET"]
)
@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")
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/add",
methods=["GET", "POST"],
)
@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
is_default = first_email_address if first_email_address else form.is_default.data
if form.validate_on_submit():
if current_user.platform_admin:
service_api_client.add_reply_to_email_address(
service_id, email_address=form.email_address.data, is_default=is_default
)
return redirect(url_for(".service_email_reply_to", service_id=service_id))
else:
try:
notification_id = service_api_client.verify_reply_to_email_address(
service_id, form.email_address.data
)["data"]["id"]
except HTTPError as e:
if e.status_code == 409:
flash(e.message, "error")
return redirect(
url_for(".service_email_reply_to", service_id=service_id)
)
else:
raise e
return redirect(
url_for(
".service_verify_reply_to_address",
service_id=service_id,
notification_id=notification_id,
is_default=is_default,
)
)
return render_template(
"views/service-settings/email-reply-to/add.html",
form=form,
first_email_address=first_email_address,
)
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify",
methods=["GET", "POST"],
)
@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)
return render_template(
"views/service-settings/email-reply-to/verify.html",
service_id=service_id,
notification_id=notification_id,
partials=get_service_verify_reply_to_address_partials(
service_id, notification_id
),
verb=("Change" if replace else "Add"),
replace=replace,
is_default=is_default,
)
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify.json"
)
@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)
)
def get_service_verify_reply_to_address_partials(service_id, notification_id):
form = ServiceReplyToEmailForm()
first_email_address = current_service.count_email_reply_to_addresses == 0
notification = notification_api_client.get_notification(
current_app.config["NOTIFY_SERVICE_ID"], notification_id
)
replace = request.args.get("replace", False)
replace = False if replace == "False" else replace
existing_is_default = False
if replace:
existing = current_service.get_email_reply_to_address(replace)
existing_is_default = existing["is_default"]
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 = VerificationStatus.SUCCESS
if notification["to"] not in [
i["email_address"] for i in current_service.email_reply_to_addresses
]:
if replace:
service_api_client.update_reply_to_email_address(
current_service.id,
replace,
email_address=notification["to"],
is_default=is_default,
)
else:
service_api_client.add_reply_to_email_address(
current_service.id,
email_address=notification["to"],
is_default=is_default,
)
seconds_since_sending = (
datetime.utcnow() - parse_naive_dt(notification["created_at"])
).seconds
if notification["status"] in FAILURE_STATUSES or (
notification["status"] in SENDING_STATUSES
and seconds_since_sending
> current_app.config["REPLY_TO_EMAIL_ADDRESS_VALIDATION_TIMEOUT"]
):
verification_status = "failure"
form.email_address.data = notification["to"]
form.is_default.data = is_default
return {
"status": render_template(
"views/service-settings/email-reply-to/_verify-updates.html",
reply_to_email_address=notification["to"],
service_id=current_service.id,
notification_id=notification_id,
verification_status=verification_status,
is_default=is_default,
existing_is_default=existing_is_default,
form=form,
first_email_address=first_email_address,
replace=replace,
),
"stop": 0 if verification_status == VerificationStatus.PENDING else 1,
}
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:reply_to_email_id>/edit",
methods=["GET", "POST"],
endpoint="service_edit_email_reply_to",
)
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:reply_to_email_id>/delete",
methods=["GET"],
endpoint="service_confirm_delete_email_reply_to",
)
@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(
reply_to_email_id
)
if request.method == "GET":
form.email_address.data = reply_to_email_address["email_address"]
form.is_default.data = reply_to_email_address["is_default"]
show_choice_of_default_checkbox = not reply_to_email_address["is_default"]
if form.validate_on_submit():
if (
form.email_address.data == reply_to_email_address["email_address"]
or current_user.platform_admin
):
service_api_client.update_reply_to_email_address(
current_service.id,
reply_to_email_id=reply_to_email_id,
email_address=form.email_address.data,
is_default=(
True
if reply_to_email_address["is_default"]
else form.is_default.data
),
)
return redirect(url_for(".service_email_reply_to", service_id=service_id))
try:
notification_id = service_api_client.verify_reply_to_email_address(
service_id, form.email_address.data
)["data"]["id"]
except HTTPError as e:
if e.status_code == 409:
flash(e.message, "error")
return redirect(
url_for(".service_email_reply_to", service_id=service_id)
)
else:
raise e
return redirect(
url_for(
".service_verify_reply_to_address",
service_id=service_id,
notification_id=notification_id,
is_default=(
True
if reply_to_email_address["is_default"]
else form.is_default.data
),
replace=reply_to_email_id,
)
)
if request.endpoint == "main.service_confirm_delete_email_reply_to":
flash("Are you sure you want to delete this reply-to email address?", "delete")
return render_template(
"views/service-settings/email-reply-to/edit.html",
form=form,
reply_to_email_address_id=reply_to_email_id,
show_choice_of_default_checkbox=show_choice_of_default_checkbox,
)
@main.route(
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:reply_to_email_id>/delete",
methods=["POST"],
)
@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,
reply_to_email_id=reply_to_email_id,
)
return redirect(url_for(".service_email_reply_to", service_id=service_id))
@main.route(
"/services/<uuid:service_id>/service-settings/set-inbound-number",
methods=["GET", "POST"],
)
@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()
)
inbound_numbers_value_and_label = [
(number["id"], number["number"]) for number in available_inbound_numbers["data"]
]
no_available_numbers = available_inbound_numbers["data"] == []
form = AdminServiceInboundNumberForm(
inbound_number_choices=inbound_numbers_value_and_label
)
if form.validate_on_submit():
service_api_client.add_sms_sender(
current_service.id,
sms_sender=form.inbound_number.data,
is_default=True,
inbound_number_id=form.inbound_number.data,
)
current_service.force_permission(ServicePermission.INBOUND_SMS, on=True)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-inbound-number.html",
form=form,
no_available_numbers=no_available_numbers,
)
@main.route(
"/services/<uuid:service_id>/service-settings/sms-prefix", methods=["GET", "POST"]
)
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
def service_set_sms_prefix(service_id):
form = SMSPrefixForm(enabled=current_service.prefix_sms)
form.enabled.label.text = "Start all text messages with {}:".format(
current_service.name
)
if form.validate_on_submit():
current_service.update(prefix_sms=form.enabled.data)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template("views/service-settings/sms-prefix.html", form=form)
@main.route(
"/services/<uuid:service_id>/service-settings/set-international-sms",
methods=["GET", "POST"],
)
@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",
enabled=current_service.has_permission(ServicePermission.INTERNATIONAL_SMS),
)
if form.validate_on_submit():
current_service.force_permission(
ServicePermission.INTERNATIONAL_SMS,
on=form.enabled.data,
)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-international-sms.html",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/set-inbound-sms", methods=["GET"]
)
@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",
)
@main.route(
"/services/<uuid:service_id>/service-settings/set-<channel>",
methods=["GET", "POST"],
)
@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)
form = ServiceSwitchChannelForm(
channel=channel, enabled=current_service.has_permission(channel)
)
if form.validate_on_submit():
current_service.force_permission(
channel,
on=form.enabled.data,
)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-{}.html".format(channel),
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/set-auth-type", methods=["GET"]
)
@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",
)
@main.route("/services/<uuid:service_id>/service-settings/sms-sender", methods=["GET"])
@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",
)
@main.route(
"/services/<uuid:service_id>/service-settings/sms-sender/add",
methods=["GET", "POST"],
)
@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
if form.validate_on_submit():
service_api_client.add_sms_sender(
current_service.id,
sms_sender=form.sms_sender.data.replace("\r", "") or None,
is_default=first_sms_sender if first_sms_sender else form.is_default.data,
)
return redirect(url_for(".service_sms_senders", service_id=service_id))
return render_template(
"views/service-settings/sms-sender/add.html",
form=form,
first_sms_sender=first_sms_sender,
)
@main.route(
"/services/<uuid:service_id>/service-settings/sms-sender/<uuid:sms_sender_id>/edit",
methods=["GET", "POST"],
endpoint="service_edit_sms_sender",
)
@main.route(
"/services/<uuid:service_id>/service-settings/sms-sender/<uuid:sms_sender_id>/delete",
methods=["GET"],
endpoint="service_confirm_delete_sms_sender",
)
@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"]
if is_inbound_number:
form = ServiceEditInboundNumberForm(is_default=sms_sender["is_default"])
else:
form = ServiceSmsSenderForm(**sms_sender)
if form.validate_on_submit():
service_api_client.update_sms_sender(
current_service.id,
sms_sender_id=sms_sender_id,
sms_sender=(
sms_sender["sms_sender"]
if is_inbound_number
else form.sms_sender.data.replace("\r", "")
),
is_default=True if sms_sender["is_default"] else form.is_default.data,
)
return redirect(url_for(".service_sms_senders", service_id=service_id))
form.is_default.data = sms_sender["is_default"]
if request.endpoint == "main.service_confirm_delete_sms_sender":
flash("Are you sure you want to delete this text message sender?", "delete")
return render_template(
"views/service-settings/sms-sender/edit.html",
form=form,
sms_sender=sms_sender,
inbound_number=is_inbound_number,
sms_sender_id=sms_sender_id,
)
@main.route(
"/services/<uuid:service_id>/service-settings/sms-sender/<uuid:sms_sender_id>/delete",
methods=["POST"],
)
@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,
sms_sender_id=sms_sender_id,
)
return redirect(url_for(".service_sms_senders", service_id=service_id))
@main.route(
"/services/<uuid:service_id>/service-settings/set-free-sms-allowance",
methods=["GET", "POST"],
)
@user_is_platform_admin
def set_free_sms_allowance(service_id):
form = AdminServiceSMSAllowanceForm(
free_sms_allowance=current_service.free_sms_fragment_limit
)
if form.validate_on_submit():
billing_api_client.create_or_update_free_sms_fragment_limit(
service_id, form.free_sms_allowance.data
)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-free-sms-allowance.html",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/set-message-limit",
methods=["GET", "POST"],
)
@user_is_platform_admin
def set_message_limit(service_id):
form = AdminServiceMessageLimitForm(message_limit=current_service.message_limit)
if form.validate_on_submit():
current_service.update(message_limit=form.message_limit.data)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-message-limit.html",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/set-rate-limit",
methods=["GET", "POST"],
)
@user_is_platform_admin
def set_rate_limit(service_id):
form = AdminServiceRateLimitForm(rate_limit=current_service.rate_limit)
if form.validate_on_submit():
current_service.update(rate_limit=form.rate_limit.data)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/set-rate-limit.html",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/link-service-to-organization",
methods=["GET", "POST"],
)
@user_is_platform_admin
def link_service_to_organization(service_id):
all_organizations = organizations_client.get_organizations()
form = AdminSetOrganizationForm(
choices=convert_dictionary_to_wtforms_choices_format(
all_organizations, "id", "name"
),
organizations=current_service.organization_id,
)
if form.validate_on_submit():
if form.organizations.data != current_service.organization_id:
organizations_client.update_service_organization(
service_id, form.organizations.data
)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/link-service-to-organization.html",
has_organizations=all_organizations,
form=form,
search_form=SearchByNameForm(),
)
@main.route("/services/<uuid:service_id>/data-retention", methods=["GET"])
@user_is_platform_admin
def data_retention(service_id):
return render_template(
"views/service-settings/data-retention.html",
)
@main.route("/services/<uuid:service_id>/data-retention/add", methods=["GET", "POST"])
@user_is_platform_admin
def add_data_retention(service_id):
form = AdminServiceAddDataRetentionForm()
if form.validate_on_submit():
service_api_client.create_service_data_retention(
service_id, form.notification_type.data, form.days_of_retention.data
)
return redirect(url_for(".data_retention", service_id=service_id))
return render_template("views/service-settings/data-retention/add.html", form=form)
@main.route(
"/services/<uuid:service_id>/data-retention/<uuid:data_retention_id>/edit",
methods=["GET", "POST"],
)
@user_is_platform_admin
def edit_data_retention(service_id, data_retention_id):
data_retention_item = current_service.get_data_retention_item(data_retention_id)
form = AdminServiceEditDataRetentionForm(
days_of_retention=data_retention_item["days_of_retention"]
)
if form.validate_on_submit():
service_api_client.update_service_data_retention(
service_id, data_retention_id, form.days_of_retention.data
)
return redirect(url_for(".data_retention", service_id=service_id))
return render_template(
"views/service-settings/data-retention/edit.html",
form=form,
data_retention_id=data_retention_id,
notification_type=data_retention_item["notification_type"],
)
@main.route("/services/<uuid:service_id>/notes", methods=["GET", "POST"])
@user_is_platform_admin
def edit_service_notes(service_id):
form = AdminNotesForm(notes=current_service.notes)
if form.validate_on_submit():
if form.notes.data == current_service.notes:
return redirect(url_for(".service_settings", service_id=service_id))
current_service.update(notes=form.notes.data)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/edit-service-notes.html",
form=form,
)
@main.route("/services/<uuid:service_id>/edit-billing-details", methods=["GET", "POST"])
@user_is_platform_admin
def edit_service_billing_details(service_id):
form = AdminBillingDetailsForm(
billing_contact_email_addresses=current_service.billing_contact_email_addresses,
billing_contact_names=current_service.billing_contact_names,
billing_reference=current_service.billing_reference,
purchase_order_number=current_service.purchase_order_number,
notes=current_service.notes,
)
if form.validate_on_submit():
current_service.update(
billing_contact_email_addresses=form.billing_contact_email_addresses.data,
billing_contact_names=form.billing_contact_names.data,
billing_reference=form.billing_reference.data,
purchase_order_number=form.purchase_order_number.data,
notes=form.notes.data,
)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/edit-service-billing-details.html",
form=form,
)
def convert_dictionary_to_wtforms_choices_format(dictionary, value, label):
return [(item[value], item[label]) for item in dictionary]
def check_contact_details_type(contact_details):
if contact_details.startswith("http"):
return "url"
elif "@" in contact_details:
return "email_address"
else:
return "phone_number"