diff --git a/Dockerfile.debian b/Dockerfile.debian
index a574f1bc..f7da9542 100644
--- a/Dockerfile.debian
+++ b/Dockerfile.debian
@@ -17,7 +17,7 @@ ARG POETRY_VERSION=1.8.3
RUN pip install --root-user-action=ignore poetry==${POETRY_VERSION}
COPY poetry.lock pyproject.toml ./
RUN poetry config virtualenvs.create false \
- && poetry install --no-interaction --no-ansi --only main
+ && poetry install --only main --no-interaction --no-ansi
# Create a non-root user.
ARG UID=10001
diff --git a/README.md b/README.md
index 2c440ff6..61c8d3ca 100644
--- a/README.md
+++ b/README.md
@@ -117,4 +117,8 @@ install pre-commit hooks locally like so:
pre-commit install
+To run Python commands in the virtualenv, thereafter run them like so:
+
+ python manage.py
+
Reference:
diff --git a/poetry.lock b/poetry.lock
index 260ef57a..613400b7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -763,13 +763,13 @@ typing_extensions = ">=3.10.0.0"
[[package]]
name = "django"
-version = "4.2.15"
+version = "4.2.16"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.8"
files = [
- {file = "Django-4.2.15-py3-none-any.whl", hash = "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30"},
- {file = "Django-4.2.15.tar.gz", hash = "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a"},
+ {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"},
+ {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"},
]
[package.dependencies]
@@ -879,13 +879,13 @@ sftp = ["paramiko (>=1.15)"]
[[package]]
name = "django-taggit"
-version = "6.0.0"
+version = "6.1.0"
description = "django-taggit is a reusable Django application for simple tagging."
optional = false
python-versions = ">=3.8"
files = [
- {file = "django_taggit-6.0.0-py3-none-any.whl", hash = "sha256:7094f797b7e5e3727525a0af7bc550860000ddc2248266529d568eca61b39fb1"},
- {file = "django_taggit-6.0.0.tar.gz", hash = "sha256:723d98bd5c536daa3c0e1bdae0965f7005a9b15269816bb4053fafec4ebad57e"},
+ {file = "django_taggit-6.1.0-py3-none-any.whl", hash = "sha256:ab776264bbc76cb3d7e49e1bf9054962457831bd21c3a42db9138b41956e4cf0"},
+ {file = "django_taggit-6.1.0.tar.gz", hash = "sha256:c4d1199e6df34125dd36db5eb0efe545b254dec3980ce5dd80e6bab3e78757c3"},
]
[package.dependencies]
@@ -1669,24 +1669,24 @@ wcwidth = "*"
[[package]]
name = "psycopg"
-version = "3.2.2"
+version = "3.2.3"
description = "PostgreSQL database adapter for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "psycopg-3.2.2-py3-none-any.whl", hash = "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2"},
- {file = "psycopg-3.2.2.tar.gz", hash = "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0"},
+ {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"},
+ {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"},
]
[package.dependencies]
-psycopg-binary = {version = "3.2.2", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""}
+psycopg-binary = {version = "3.2.3", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""}
psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""}
typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""}
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
-binary = ["psycopg-binary (==3.2.2)"]
-c = ["psycopg-c (==3.2.2)"]
+binary = ["psycopg-binary (==3.2.3)"]
+c = ["psycopg-c (==3.2.3)"]
dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
pool = ["psycopg-pool"]
@@ -1694,75 +1694,75 @@ test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)",
[[package]]
name = "psycopg-binary"
-version = "3.2.2"
+version = "3.2.3"
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
optional = false
python-versions = ">=3.8"
files = [
- {file = "psycopg_binary-3.2.2-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8eacbf58d4f8d7bc82e0a60476afa2622b5a58f639a3cc2710e3e37b72aff3cb"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d07e62476ee8c54853b2b8cfdf3858a574218103b4cd213211f64326c7812437"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c22e615ee0ecfc6687bb8a39a4ed9d6bac030b5e72ac15e7324fd6e48979af71"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec29c7ec136263628e3f09a53e51d0a4b1ad765a6e45135707bfa848b39113f9"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:035753f80cbbf6aceca6386f53e139df70c7aca057b0592711047b5a8cfef8bb"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ee99336151ff7c30682f2ef9cb1174d235bc1471322faabba97f9db1398167"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a60674dff4a4194e88312b463fb84ac80924c2b9e25d0e0460f3176bf1af4a6b"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3c701507a49340de422d77a6ce95918a0019990bbf27daec35aa40050c6eadb6"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b3c5a04eaf8866e399315cff2e810260cce10b797437a9f49fd71b5f4b94d0a"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ad9c09de4c262f516ae6891d042a4325649b18efa39dd82bbe0f7bc95c37bfb"},
- {file = "psycopg_binary-3.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:bf1d3582185cb43ecc27403bee2f5405b7a45ccaab46c8508d9a9327341574fc"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:554d208757129d34fa47b7c890f9ef922f754e99c6b089cb3a209aa0fe282682"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:71dc3cc10d1fd7d26a3079d0a5b4a8e8ad0d7b89a702ceb7605a52e4395be122"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86f578d63f2e1fdf87c9adaed4ff23d7919bda8791cf1380fa4cf3a857ccb8b"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a4eb737682c02a602a12aa85a492608066f77793dab681b1c4e885fedc160b1"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e120a576e74e4e612c48f4b021e322e320ca102534d78a0ca4db2ffd058ae8d"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849d518e7d4c6186e1e48ea2ac2671912edf7e732fffe6f01dfed61cf0245de4"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ee2b19152bcec8f356f989c31768702be5f139b4d51094273c4a9ddc8c55380"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:00273dd011892e8216fcef76b42f775ddaa6348664a7fffae2a27c9557f45bfa"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4bcb489615d7e56d1de42937e6a0fc13f766505729afdb54c2947a52db295220"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06963f88916a177df95aaed27101af0989ba206654743b1a0e050b9d8e734686"},
- {file = "psycopg_binary-3.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed1ad836a0c21890c7f84e73c7ef1ed0950e0e4b0d8e49b609b6fd9c13f2ca21"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0dd314229885a81f9497875295d8788e651b78945627540f1e78ed71595e614a"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:989acbe2f552769cdb780346cea32d86e7c117044238d5172ac10b025fe47194"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566b1c530898590f0ac9d949cf94351c08d73c89f8800c74c0a63ffd89a383c8"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68d03efab7e2830a0df3aa4c29a708930e3f6b9fd98774ff9c4fd1f33deafecc"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e1f013bfb744023df23750fde51edcb606def8328473361db3c192c392c6060"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a06136aab55a2de7dd4e2555badae276846827cfb023e6ba1b22f7a7b88e3f1b"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:020c5154be144a1440cf87eae012b9004fb414ae4b9e7b1b9fb808fe39e96e83"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef341c556aeaa43a2729b07b04e20bfffdcf3d96c4a96e728ca94fe4ce632d8c"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66de2dd7d37bf66eb234ca9d907f5cd8caca43ff8d8a50dd5c15844d1cf0390c"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2eb6f8f410dbbb71b8c633f283b8588b63bee0a7321f00ab76e9c800c593f732"},
- {file = "psycopg_binary-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b45553c6b614d02e1486585980afdfd18f0000aac668e2e87c6e32da1adb051a"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:1ee891287c2da57e7fee31fbe2fbcdf57125768133d811b02e9523d5a052eb28"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5e95e4a8076ac7611e571623e1113fa84fd48c0459601969ffbf534d7aa236e7"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6269d79a3d7d76b6fcf0fafae8444da00e83777a6c68c43851351a571ad37155"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6dd5d21a298c3c53af20ced8da4ae4cd038c6fe88c80842a8888fa3660b2094"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cf64e41e238620f05aad862f06bc8424f8f320d8075f1499bd85a225d18bd57"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c482c3236ded54add31136a91d5223b233ec301f297fa2db79747404222dca6"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0718be095cefdad712542169d16fa58b3bd9200a3de1b0217ae761cdec1cf569"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fb303b03c243a9041e1873b596e246f7caaf01710b312fafa65b1db5cd77dd6f"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:705da5bc4364bd7529473225fca02b795653bc5bd824dbe43e1df0b1a40fe691"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:05406b96139912574571b1c56bb023839a9146cf4b57c4548f36251dd5909fa1"},
- {file = "psycopg_binary-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:7c357cf87e8d7612cfe781225be7669f35038a765d1b53ec9605f6c5aef9ee85"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:059aa5e8fa119de328b4cb02ee80775443763b25682a02dd7d026b8d4f565834"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05a50f94e1e4fa37a0074b09263b83b0aa038c3c72068a61f1ad61ea449ef9d5"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:951507b3d77a64c907afe893e01e09b41051fd7e27e9462f450fb8bb64bc22b0"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ec4986c4ac2503e865acd3943d179531c3bbfa5a1c8ee81fcfccb551dad645f"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b32b0e838841d5b109d32fc706b8bc64e50c161fee3f1371ccf696e5598bc49"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fdc74a83348477b28bea9e7b391c9fc189b480fe3cd0e46bb989514410b64d60"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9efe0ca78be4a573b4b81226904c711cfadc4783d64bfdf58a3394da7c1a1354"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:51f56ae2898acaa33623adad96ddc5acbb5e2f72f2fc020065c8be05c0e01dce"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:43b209be0424e8abece428a884cb711f504e3526dfbcb0bf51529907a55eda15"},
- {file = "psycopg_binary-3.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:d3c147eea9f3950a34133dc187e8d3534e54ff4a178a4ebd8993b2c97e123200"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6c7b6a8d4e1b77cdb50192b61235b33fc2f1d28c67627fc93a1d43e9130dd479"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e234edc4bb746d8ac3daae8753ee38eaa7af2ee333a1d35ce6b02a02874aed18"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f12640ba92c538b3b64a199a918d3bb0cc0d7f7123c6ba93cb065e1a2d049f0"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8937dc548621b336b0d8383a3470fb7192b42a108c760a152282909867bf5b26"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4afbb97d64cd8078edec859b07859a18ef3de7261a3a873ba52f32548373ae92"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c432710bdf8ccfdd75b0bc9cdf1fd21ff394363e4daec099c667f3c5f1721e2b"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:366cc4e194f7feb4e3038d6775fd4b69835e7d923972aee5baec986de972abd6"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b286ed65a891928bd457ffa0cd5fec09b9b5208bfd096d087e45369f07c5cb85"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fee41c99312002e5d1f7462b1954aefed44c6efe5f021c3eac311640c16f6b7"},
- {file = "psycopg_binary-3.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:87cceaf07760a04023596f9ca1d4e929d38ae8d778161cb3e8d27a0f990dd264"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea"},
+ {file = "psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585"},
+ {file = "psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9"},
+ {file = "psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd"},
+ {file = "psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b"},
+ {file = "psycopg_binary-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f"},
+ {file = "psycopg_binary-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749"},
]
[[package]]
@@ -1969,18 +1969,18 @@ files = [
[[package]]
name = "redis"
-version = "5.0.8"
+version = "5.1.0"
description = "Python client for Redis database and key-value store"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
- {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
+ {file = "redis-5.1.0-py3-none-any.whl", hash = "sha256:fd4fccba0d7f6aa48c58a78d76ddb4afc698f5da4a2c1d03d916e4fd7ab88cdd"},
+ {file = "redis-5.1.0.tar.gz", hash = "sha256:b756df1e4a3858fcc0ef861f3fc53623a96c41e2b1f5304e09e0fe758d333d40"},
]
[package.extras]
-hiredis = ["hiredis (>1.0.0)"]
-ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
+hiredis = ["hiredis (>=3.0.0)"]
+ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"]
[[package]]
name = "requests"
@@ -2332,4 +2332,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "38d05c5a9700e1d71634d29a93a67f15cfce109a5788debf019e778a7c51c5fc"
+content-hash = "dc6b1a361b6d3b8c7b8b8886017fd21667b06ea16a8420430ec98a5ab58f69a5"
diff --git a/prs2/referral/management/commands/load_lgas.py b/prs2/referral/management/commands/load_lgas.py
deleted file mode 100644
index 3ffad7f0..00000000
--- a/prs2/referral/management/commands/load_lgas.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import csv
-from django.core.management.base import BaseCommand, CommandError
-from referral.models import LocalGovernment
-
-
-class Command(BaseCommand):
- help = 'Loads LGA names from a CSV (lga.csv)'
-
- def handle(self, *args, **options):
- try:
- lga_file = open('lga.csv', 'r')
- except IOError:
- raise CommandError('lga.csv file not present')
-
- lga_reader = csv.reader(lga_file)
- for row in lga_reader:
- LocalGovernment.objects.create(name=row[0])
-
- self.stdout.write('Done!')
diff --git a/prs2/referral/management/commands/load_region_geom.py b/prs2/referral/management/commands/load_region_geom.py
index 3746f726..b827d293 100644
--- a/prs2/referral/management/commands/load_region_geom.py
+++ b/prs2/referral/management/commands/load_region_geom.py
@@ -1,22 +1,43 @@
from django.contrib.gis.geos import GEOSGeometry
-from django.core.management.base import BaseCommand, CommandError
-import json
+from django.core.management.base import BaseCommand
from referral.models import Region
+from referral.utils import wfs_getfeature
class Command(BaseCommand):
- help = 'Loads Region geometry from serialised GeoJSON (dpaw_regions.json)'
+ help = "Loads Region geometry from a Geoserver layer WFS"
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--typename",
+ action="store",
+ required=True,
+ type=str,
+ dest="typename",
+ help="typeName value for WFS GetFeature request (namespace:featuretype)",
+ )
+ parser.add_argument(
+ "--field",
+ action="store",
+ required=True,
+ type=str,
+ dest="field",
+ help="GeoJSON property key containing the region name",
+ )
def handle(self, *args, **options):
- try:
- regions_json = json.load(open('dpaw_regions.json', 'r'))
- except IOError:
- raise CommandError('dpaw_regions.json file not present')
+ type_name = options["typename"]
+ field = options["field"]
+ regions_data = wfs_getfeature(type_name)
+
+ if "features" not in regions_data:
+ self.stdout.write("No data returned")
+ return
- for f in regions_json['features']:
- region = Region.objects.get(name__istartswith=f['properties']['region'])
- region.region_mpoly = GEOSGeometry(json.dumps(f['geometry']))
+ for feature in regions_data["features"]:
+ region = Region.objects.get(name__iexact=feature["properties"][field])
+ region.region_mpoly = GEOSGeometry(str(feature["geometry"]))
region.save()
- self.stdout.write('{} geometry updated'.format(region))
+ self.stdout.write("{} region geometry updated".format(region))
- self.stdout.write('Done!')
+ self.stdout.write("Completed")
diff --git a/prs2/referral/management/commands/overdue_tasks_email.py b/prs2/referral/management/commands/overdue_tasks_email.py
index 1c7a57c6..2c99ebf3 100644
--- a/prs2/referral/management/commands/overdue_tasks_email.py
+++ b/prs2/referral/management/commands/overdue_tasks_email.py
@@ -3,11 +3,11 @@
class Command(BaseCommand):
- help = 'Send email to users notifying about overdue tasks'
+ help = "Send email to users notifying about overdue tasks"
def handle(self, *args, **options):
try:
overdue_task_email()
- self.stdout.write('Done')
+ self.stdout.write("Done")
except Exception:
- raise CommandError('Unable to send overdue tasks email to users')
+ raise CommandError("Unable to send overdue tasks email to users")
diff --git a/prs2/referral/utils.py b/prs2/referral/utils.py
index 6a85f724..4cfadc6f 100644
--- a/prs2/referral/utils.py
+++ b/prs2/referral/utils.py
@@ -1,4 +1,8 @@
-from datetime import datetime, date
+import json
+import re
+from datetime import date, datetime
+
+import requests
from dbca_utils.utils import env
from django.apps import apps
from django.conf import settings
@@ -8,10 +12,7 @@
from django.db.models.base import ModelBase
from django.utils.encoding import smart_str
from django.utils.safestring import mark_safe
-import json
from reversion.models import Version
-import re
-import requests
from unidecode import unidecode
@@ -177,9 +178,7 @@ def user_referral_history(user, referral):
else:
new_ref_history.append(i)
# Add the passed-in referral to the end of the new list.
- new_ref_history.append(
- [referral.id, datetime.strftime(datetime.today(), "%d-%m-%Y")]
- )
+ new_ref_history.append([referral.id, datetime.strftime(datetime.today(), "%d-%m-%Y")])
# History can be a maximum of 20 referrals; slice the new list accordingly.
if len(new_ref_history) > 20:
new_ref_history = new_ref_history[-20:]
@@ -189,16 +188,13 @@ def user_referral_history(user, referral):
def user_task_history(user, task, comment=None):
- """Utility function to update the task history in a user's profile.
- """
+ """Utility function to update the task history in a user's profile."""
profile = user.userprofile
if not profile.task_history:
task_history = []
else:
task_history = json.loads(profile.task_history)
- task_history.append(
- [task.pk, datetime.strftime(datetime.today(), "%d-%m-%Y"), comment]
- )
+ task_history.append([task.pk, datetime.strftime(datetime.today(), "%d-%m-%Y"), comment])
profile.task_history = json.dumps(task_history)
profile.save()
@@ -231,9 +227,7 @@ def is_prs_power_user(request):
def prs_user(request):
- return (
- is_prs_user(request) or is_prs_power_user(request) or request.user.is_superuser
- )
+ return is_prs_user(request) or is_prs_power_user(request) or request.user.is_superuser
def update_revision_history(app_model):
@@ -265,10 +259,10 @@ def update_revision_history(app_model):
def overdue_task_email():
- """A utility function to send an email to each user with tasks that are overdue.
- """
+ """A utility function to send an email to each user with tasks that are overdue."""
from django.contrib.auth.models import Group
- from .models import TaskState, Task
+
+ from .models import Task, TaskState
prs_grp = Group.objects.get(name=settings.PRS_USER_GROUP)
users = prs_grp.user_set.filter(is_active=True)
@@ -295,20 +289,14 @@ def overdue_task_email():
assigned to you within PRS are currently overdue:
"""
for t in ongoing_tasks:
- text_content += "* Referral ID {} - {}\n".format(
- t.referral.pk, t.type.name
- )
+ text_content += "* Referral ID {} - {}\n".format(t.referral.pk, t.type.name)
html_content += '- Referral ID {} - {}
'.format(
settings.SITE_URL + t.referral.get_absolute_url(),
t.referral.pk,
t.type.name,
)
- text_content += (
- "This is an automatically-generated email - please do not reply.\n"
- )
- html_content += (
- "
This is an automatically-generated email - please do not reply.
"
- )
+ text_content += "This is an automatically-generated email - please do not reply.\n"
+ html_content += "This is an automatically-generated email - please do not reply.
"
msg = EmailMultiAlternatives(subject, text_content, from_email, to_email)
msg.attach_alternative(html_content, "text/html")
# Email should fail gracefully - ie no Exception raised on failure.
@@ -317,35 +305,45 @@ def overdue_task_email():
return True
-def query_cadastre(cql_filter, crs="EPSG:4326"):
- """A utility function to query the internal DBCA Cadastre dataset via the passed-in CQL filter
+def wfs_getfeature(type_name, crs="EPSG:4326", cql_filter=None, max_features=50):
+ """A utility function to perform a GetFeature request on a WFS endpoint
and return results as GeoJSON.
"""
- url = env('GEOSERVER_URL', None)
- auth = (env('GEOSERVER_SSO_USER', None), env('GEOSERVER_SSO_PASS', None))
- type_name = env('CADASTRE_LAYER_NAME', '')
+ url = env("GEOSERVER_URL", None)
+ auth = (env("GEOSERVER_SSO_USER", None), env("GEOSERVER_SSO_PASS", None))
params = {
- 'service': 'WFS',
- 'version': '2.0.0',
- 'typeName': type_name,
- 'request': 'getFeature',
- 'outputFormat': 'json',
- 'SRSName': f'urn:x-ogc:def:crs:{crs}',
- 'cql_filter': cql_filter,
+ "service": "WFS",
+ "version": "1.1.0",
+ "typeName": type_name,
+ "request": "getFeature",
+ "outputFormat": "json",
+ "SRSName": f"urn:x-ogc:def:crs:{crs}",
+ "maxFeatures": max_features,
}
+ if cql_filter:
+ params["cql_filter"] = cql_filter
resp = requests.get(url, auth=auth, params=params)
- resp.raise_for_status()
+ try:
+ resp.raise_for_status()
+ except:
+ # On exception, return an empty dict.
+ return {}
+
return resp.json()
def query_caddy(q):
- """Utility function to proxy queries to the Caddy geocoder service.
- """
- url = env('GEOCODER_URL', None)
- auth = (env('GEOSERVER_SSO_USER', None), env('GEOSERVER_SSO_PASS', None))
- params = {'q': q}
+ """Utility function to proxy queries to the Caddy geocoder service."""
+ url = env("GEOCODER_URL", None)
+ auth = (env("GEOSERVER_SSO_USER", None), env("GEOSERVER_SSO_PASS", None))
+ params = {"q": q}
resp = requests.get(url, auth=auth, params=params)
- resp.raise_for_status()
+ try:
+ resp.raise_for_status()
+ except:
+ # On exception, return an empty list.
+ return []
+
return resp.json()
diff --git a/prs2/referral/views.py b/prs2/referral/views.py
index ede3d78c..a3ab4951 100644
--- a/prs2/referral/views.py
+++ b/prs2/referral/views.py
@@ -1,5 +1,10 @@
+import json
+import logging
+import re
from copy import copy
-from datetime import datetime, date, timedelta
+from datetime import date, datetime, timedelta
+
+from dbca_utils.utils import env
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -10,82 +15,65 @@
from django.core.mail import EmailMultiAlternatives
from django.core.paginator import Paginator
from django.core.serializers import serialize
-from django.urls import reverse
-from django.db.models import Q, F
-from django.http import (
- HttpResponse,
- HttpResponseRedirect,
- JsonResponse,
- HttpResponseForbidden,
- HttpResponseBadRequest,
-)
+from django.db.models import F, Q
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect
+from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_exempt
-from django.views.generic import View, ListView, TemplateView, FormView
-import json
-import logging
-import re
-from taggit.models import Tag
-
+from django.views.generic import FormView, ListView, TemplateView, View
+from indexer.utils import typesense_client
+from referral.forms import (
+ ClearanceCreateForm,
+ IntersectingReferralForm,
+ LocationForm,
+ ReferralCreateForm,
+ TagReplaceForm,
+ TaskCancelForm,
+ TaskCompleteForm,
+ TaskCreateForm,
+ TaskForm,
+ TaskInheritForm,
+ TaskReassignForm,
+ TaskStartForm,
+ TaskStopForm,
+)
from referral.models import (
- Task,
+ Agency,
+ Bookmark,
Clearance,
- Referral,
Condition,
+ Location,
Note,
+ Organisation,
Record,
- Location,
- Bookmark,
+ Referral,
+ ReferralType,
RelatedReferral,
+ Task,
TaskState,
- Organisation,
TaskType,
- Agency,
- ReferralType,
)
from referral.utils import (
- is_model_or_string,
breadcrumbs_li,
- smart_truncate,
- user_task_history,
- user_referral_history,
- prs_user,
+ is_model_or_string,
is_prs_power_user,
- query_cadastre,
+ prs_user,
query_caddy,
+ smart_truncate,
+ user_referral_history,
+ user_task_history,
+ wfs_getfeature,
)
-from referral.forms import (
- ReferralCreateForm,
- TaskCreateForm,
- ClearanceCreateForm,
- LocationForm,
- TaskForm,
- TaskCompleteForm,
- TaskStopForm,
- TaskStartForm,
- TaskReassignForm,
- TaskCancelForm,
- TaskInheritForm,
- IntersectingReferralForm,
- TagReplaceForm,
-)
-from referral.views_base import (
- PrsObjectDetail,
- PrsObjectList,
- PrsObjectCreate,
- PrsObjectUpdate,
- PrsObjectDelete,
-)
-from indexer.utils import typesense_client
+from referral.views_base import PrsObjectCreate, PrsObjectDelete, PrsObjectDetail, PrsObjectList, PrsObjectUpdate
+from taggit.models import Tag
LOGGER = logging.getLogger("prs")
class SiteHome(LoginRequiredMixin, ListView):
- """Site home page view. Returns an object list of tasks (ongoing or stopped).
- """
+ """Site home page view. Returns an object list of tasks (ongoing or stopped)."""
stopped_tasks = False
printable = False
@@ -110,9 +98,7 @@ def get_context_data(self, **kwargs):
context["headers"] = copy(Task.headers_site_home)
if not self.stopped_tasks:
context["stopped_tasks_exist"] = (
- Task.objects.current()
- .filter(assigned_user=self.request.user, state__name="Stopped")
- .exists()
+ Task.objects.current().filter(assigned_user=self.request.user, state__name="Stopped").exists()
)
# Printable view only: pop the last element from 'headers'
if "print" in self.request.GET or self.printable:
@@ -123,8 +109,7 @@ def get_context_data(self, **kwargs):
class HelpPage(LoginRequiredMixin, TemplateView):
- """Help page (static template view).
- """
+ """Help page (static template view)."""
template_name = "help_page.html"
@@ -161,11 +146,11 @@ def get_context_data(self, **kwargs):
client = typesense_client()
page = self.request.GET.get("page", 1)
search_q = {
- 'q': self.request.GET["q"],
- 'sort_by': 'created:desc',
- 'num_typos': 0,
- 'page': page,
- 'per_page': 20,
+ "q": self.request.GET["q"],
+ "sort_by": "created:desc",
+ "num_typos": 0,
+ "page": page,
+ "per_page": 20,
}
if collection == "referrals":
@@ -196,57 +181,67 @@ def get_context_data(self, **kwargs):
# Replace underscores in search field names with spaces.
highlights.append((key.replace("_", " "), value["snippet"]))
hit["highlights"] = highlights
- context["search_result"].append({
- "object": Referral.objects.get(pk=hit["document"]["id"]),
- "highlights": highlights,
- })
+ context["search_result"].append(
+ {
+ "object": Referral.objects.get(pk=hit["document"]["id"]),
+ "highlights": highlights,
+ }
+ )
elif collection == "records":
for hit in search_result["hits"]:
highlights = []
for key, value in hit["highlight"].items():
highlights.append((key.replace("_", " "), value["snippet"]))
hit["highlights"] = highlights
- context["search_result"].append({
- "object": Record.objects.get(pk=hit["document"]["id"]),
- "highlights": highlights,
- })
+ context["search_result"].append(
+ {
+ "object": Record.objects.get(pk=hit["document"]["id"]),
+ "highlights": highlights,
+ }
+ )
elif collection == "notes":
for hit in search_result["hits"]:
highlights = []
for key, value in hit["highlight"].items():
highlights.append((key.replace("_", " "), value["snippet"]))
hit["highlights"] = highlights
- context["search_result"].append({
- "object": Note.objects.get(pk=hit["document"]["id"]),
- "highlights": highlights,
- })
+ context["search_result"].append(
+ {
+ "object": Note.objects.get(pk=hit["document"]["id"]),
+ "highlights": highlights,
+ }
+ )
elif collection == "tasks":
for hit in search_result["hits"]:
highlights = []
for key, value in hit["highlight"].items():
highlights.append((key.replace("_", " "), value["snippet"]))
hit["highlights"] = highlights
- context["search_result"].append({
- "object": Task.objects.get(pk=hit["document"]["id"]),
- "highlights": highlights,
- })
+ context["search_result"].append(
+ {
+ "object": Task.objects.get(pk=hit["document"]["id"]),
+ "highlights": highlights,
+ }
+ )
elif collection == "conditions":
for hit in search_result["hits"]:
highlights = []
for key, value in hit["highlight"].items():
highlights.append((key.replace("_", " "), value["snippet"]))
hit["highlights"] = highlights
- context["search_result"].append({
- "object": Condition.objects.get(pk=hit["document"]["id"]),
- "highlights": highlights,
- })
+ context["search_result"].append(
+ {
+ "object": Condition.objects.get(pk=hit["document"]["id"]),
+ "highlights": highlights,
+ }
+ )
return context
class IndexSearchCombined(LoginRequiredMixin, TemplateView):
- """A combined version of the index search which returns referrals with linked objects.
- """
+ """A combined version of the index search which returns referrals with linked objects."""
+
template_name = "referral/prs_index_search_combined.html"
def get_context_data(self, **kwargs):
@@ -264,9 +259,9 @@ def get_context_data(self, **kwargs):
context["referral_headers"] = Referral.headers
client = typesense_client()
search_q = {
- 'q': self.request.GET["q"],
- 'sort_by': 'created:desc',
- 'num_typos': 0,
+ "q": self.request.GET["q"],
+ "sort_by": "created:desc",
+ "num_typos": 0,
}
referrals = {}
@@ -397,8 +392,7 @@ def get_context_data(self, **kwargs):
class ReferralCreate(PrsObjectCreate):
- """Dedicated create view for new referrals.
- """
+ """Dedicated create view for new referrals."""
model = Referral
form_class = ReferralCreateForm
@@ -415,9 +409,7 @@ def get_context_data(self, **kwargs):
context["title"] = "CREATE A NEW REFERRAL"
context["page_title"] = "PRS | Referrals | Create"
# Pass in a serialised list of tag names.
- context["tags"] = json.dumps(
- [t.name for t in Tag.objects.all().order_by("name")]
- )
+ context["tags"] = json.dumps([t.name for t in Tag.objects.all().order_by("name")])
return context
def get_initial(self):
@@ -474,18 +466,14 @@ def form_valid(self, form):
This task is attached to referral ID {0}.\n
The referrer's reference is {1}.\n
The referral address is {2}\n
- """.format(
- new_ref.id, new_ref.reference, address
- )
+ """.format(new_ref.id, new_ref.reference, address)
html_content = """This is an automated message to let you know
that you have been assigned a PRS task by the sending user.
This task is attached to referral ID {0}, at this URL:
{1}
The referrer's reference is: {2}.
The referral address is {3}.
- """.format(
- new_ref.pk, ref_url, new_ref.reference, address
- )
+ """.format(new_ref.pk, ref_url, new_ref.reference, address)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
msg.attach_alternative(html_content, "text/html")
# Email should fail gracefully - ie no Exception raised on failure.
@@ -497,9 +485,7 @@ def form_valid(self, form):
if req.POST.get("save"):
return redirect(new_ref.get_absolute_url())
else:
- return redirect(
- reverse("referral_location_create", kwargs={"pk": new_ref.pk})
- )
+ return redirect(reverse("referral_location_create", kwargs={"pk": new_ref.pk}))
class ReferralDetail(PrsObjectDetail):
@@ -568,9 +554,7 @@ def get_context_data(self, **kwargs):
context["rel_model"] = self.related_model
# Test if the user has bookmarked this referral.
if Bookmark.objects.filter(referral=ref, user=self.request.user).exists():
- context["bookmark"] = Bookmark.objects.filter(
- referral=ref, user=self.request.user
- )[0]
+ context["bookmark"] = Bookmark.objects.filter(referral=ref, user=self.request.user)[0]
# Add context for each child model type: task_list, note_list, etc.
table = """
@@ -589,12 +573,7 @@ def get_context_data(self, **kwargs):
headers.remove("Referral ID")
headers.append("Actions")
thead = "".join(["{} | ".format(h) for h in headers])
- rows = [
- "{}{}
".format(
- o.as_row_minus_referral(), o.as_row_actions()
- )
- for o in obj_qs
- ]
+ rows = ["{}{}
".format(o.as_row_minus_referral(), o.as_row_actions()) for o in obj_qs]
tbody = "".join(rows)
obj_tab_html = table.format(thead, tbody)
if m == Location: # Append a div for the map viewer.
@@ -603,9 +582,7 @@ def get_context_data(self, **kwargs):
context[obj_list] = obj_qs
else:
context["{}_count".format(m._meta.object_name.lower())] = 0
- context[obj_tab] = "No {} found for this referral".format(
- m._meta.verbose_name_plural
- )
+ context[obj_tab] = "No {} found for this referral".format(m._meta.verbose_name_plural)
context[obj_list] = None
# Add child locations serialised as GeoJSON (if geometry exists).
@@ -631,15 +608,11 @@ def get_context_data(self, **kwargs):
if "id" in self.kwargs: # Relating to existing object.
child_obj = child_model.objects.get(pk=self.kwargs["id"])
if self.kwargs["type"] == "addnote":
- context["title"] = "ADD EXISTING NOTE(S) TO {}".format(
- child_obj
- ).upper()
+ context["title"] = "ADD EXISTING NOTE(S) TO {}".format(child_obj).upper()
context["page_title"] = "PRS | Add note(s) to {}".format(child_obj)
last_breadcrumb = "Add note(s) to {}".format(child_obj)
elif "addrecord" in self.kwargs.values():
- context["title"] = "ADD EXISTING RECORD(S) TO {}".format(
- child_obj
- ).upper()
+ context["title"] = "ADD EXISTING RECORD(S) TO {}".format(child_obj).upper()
context["page_title"] = "PRS | Add record(s) to {}".format(child_obj)
last_breadcrumb = "Add record(s) to {}".format(child_obj)
elif "addnewnote" in self.kwargs.values():
@@ -742,7 +715,7 @@ def form_valid(self, form):
# Invalidate any cached referral detail fragment.
if settings.REDIS_CACHE_HOST:
- key = make_template_fragment_key('referral_detail', [self.object.referral.pk])
+ key = make_template_fragment_key("referral_detail", [self.object.referral.pk])
cache.delete(key)
redirect_url = redirect_url if redirect_url else self.get_success_url()
@@ -771,9 +744,7 @@ def create_existing_note(self, form):
note.records.add(obj)
messages.success(
self.request,
- "The note has been added to {} {}.".format(
- self.kwargs["model"].capitalize(), self.kwargs["id"]
- ),
+ "The note has been added to {} {}.".format(self.kwargs["model"].capitalize(), self.kwargs["id"]),
)
def create_new_note(self, form):
@@ -824,9 +795,7 @@ def create_existing_record(self, form):
record.note_set.add(obj)
messages.success(
self.request,
- "The record has been added to {} {}.".format(
- self.kwargs["model"].capitalize(), self.kwargs["id"]
- ),
+ "The record has been added to {} {}.".format(self.kwargs["model"].capitalize(), self.kwargs["id"]),
)
def create_new_record(self, form):
@@ -859,37 +828,27 @@ def create_new_record(self, form):
return redirect_url
def get_condition_choices(self):
- """Return conditions with 'approved' text only.
- """
- condition_qs = (
- Condition.objects.current()
- .filter(referral=self.parent_referral)
- .exclude(condition="")
- )
+ """Return conditions with 'approved' text only."""
+ condition_qs = Condition.objects.current().filter(referral=self.parent_referral).exclude(condition="")
condition_choices = []
for i in condition_qs:
condition_choices.append(
(
i.id,
- "{0} - {1}".format(
- i.identifier or "", smart_truncate(i.condition, 100)
- ),
+ "{0} - {1}".format(i.identifier or "", smart_truncate(i.condition, 100)),
)
)
return condition_choices
def create_clearance(self, form):
- """For each of the chosen conditions in the form, create one clearance task.
- """
+ """For each of the chosen conditions in the form, create one clearance task."""
request = self.request
tasks = []
for i in form.cleaned_data["conditions"]:
condition = Condition.objects.get(pk=i)
clearance_task = Task()
- clearance_task.type = TaskType.objects.get(
- name="Conditions clearance request"
- )
+ clearance_task.type = TaskType.objects.get(name="Conditions clearance request")
clearance_task.referral = condition.referral
clearance_task.assigned_user = form.cleaned_data["assigned_user"]
clearance_task.start_date = form.cleaned_data["start_date"]
@@ -901,34 +860,22 @@ def create_clearance(self, form):
clearance_task.due_date = date.today() + timedelta(days=clearance_task.type.target_days)
clearance_task.creator, clearance_task.modifier = request.user, request.user
clearance_task.save()
- condition.add_clearance(
- task=clearance_task, deposited_plan=form.cleaned_data["deposited_plan"]
- )
+ condition.add_clearance(task=clearance_task, deposited_plan=form.cleaned_data["deposited_plan"])
# If the user checked the "Email user" box, send them a notification.
if request.POST.get("email_user"):
- subject = "PRS referral {0} - new clearance request notification".format(
- clearance_task.referral.pk
- )
+ subject = "PRS referral {0} - new clearance request notification".format(clearance_task.referral.pk)
from_email = request.user.email
to_email = clearance_task.assigned_user.email
- referral_url = (
- settings.SITE_URL + clearance_task.referral.get_absolute_url()
- )
+ referral_url = settings.SITE_URL + clearance_task.referral.get_absolute_url()
text_content = """This is an automated message to let you know that you have
been assigned PRS clearance request {0} by the sending user.\n
This clearance request is attached to referral ID {1}.\n
- """.format(
- clearance_task.pk, clearance_task.referral.pk
- )
+ """.format(clearance_task.pk, clearance_task.referral.pk)
html_content = """This is an automated message to let you know that you have
been assigned PRS clearance request {0} by the sending user.
This task is attached to referral ID {1}, at this URL:
- {2}
""".format(
- clearance_task.pk, clearance_task.referral.pk, referral_url
- )
- msg = EmailMultiAlternatives(
- subject, text_content, from_email, [to_email]
- )
+ {2}
""".format(clearance_task.pk, clearance_task.referral.pk, referral_url)
+ msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
msg.attach_alternative(html_content, "text/html")
# Email should fail gracefully - ie no Exception raised on failure.
msg.send(fail_silently=True)
@@ -958,18 +905,10 @@ def create_condition(self, obj):
html_content += 'Condition ID {}
'.format(
settings.SITE_URL + obj.get_absolute_url(), obj.pk
)
- text_content += "The condition was created by {}.\n".format(
- obj.creator.get_full_name()
- )
- html_content += "The condition was created by {}.
".format(
- obj.creator.get_full_name()
- )
- text_content += (
- "This is an automatically-generated email - please do not reply.\n"
- )
- html_content += (
- "This is an automatically-generated email - please do not reply.
"
- )
+ text_content += "The condition was created by {}.\n".format(obj.creator.get_full_name())
+ html_content += "The condition was created by {}.
".format(obj.creator.get_full_name())
+ text_content += "This is an automatically-generated email - please do not reply.\n"
+ html_content += "This is an automatically-generated email - please do not reply.
"
msg = EmailMultiAlternatives(subject, text_content, from_email, to_email)
msg.attach_alternative(html_content, "text/html")
# Email should fail gracefully - ie no Exception raised on failure.
@@ -1101,8 +1040,7 @@ def post(self, request, *args, **kwargs):
return HttpResponseRedirect(self.get_success_url())
def polygon_intersects(self, locations):
- """ Check to see if the location polygon intersects with any other locations.
- """
+ """Check to see if the location polygon intersects with any other locations."""
intersecting_locations = []
for location in locations:
if location.poly:
@@ -1159,9 +1097,7 @@ def form_valid(self, form):
messages.success(
self.request,
- "{} Referral relationship(s) created.".format(
- len(form.cleaned_data["related_refs"])
- ),
+ "{} Referral relationship(s) created.".format(len(form.cleaned_data["related_refs"])),
)
return redirect(self.get_success_url())
@@ -1173,11 +1109,7 @@ def referral_intersecting_locations(self):
for loc_id in loc_ids:
location = get_object_or_404(Location, pk=loc_id)
geom = location.poly.wkt
- intersects = (
- Location.objects.current()
- .exclude(id=location.id)
- .filter(poly__isnull=False)
- )
+ intersects = Location.objects.current().exclude(id=location.id).filter(poly__isnull=False)
# Get a qs of intersecting locations
intersects = intersects.filter(poly__intersects=geom)
# Get a qs of referrals
@@ -1240,7 +1172,7 @@ def post(self, request, *args, **kargs):
# Invalidate the cached referral detail fragment.
if settings.REDIS_CACHE_HOST:
referral = rec.referral
- key = make_template_fragment_key('referral_detail', [referral.pk])
+ key = make_template_fragment_key("referral_detail", [referral.pk])
cache.delete(key)
return JsonResponse(
@@ -1273,9 +1205,7 @@ def get(self, request, *args, **kwargs):
task = self.get_object()
if action == "update" and task.stop_date and not task.restart_date:
- messages.error(
- request, "You can't edit a stopped task - restart the task first!"
- )
+ messages.error(request, "You can't edit a stopped task - restart the task first!")
return redirect(task.get_absolute_url())
if action == "stop" and task.complete_date:
messages.error(request, "You can't stop a completed task!")
@@ -1318,11 +1248,7 @@ def get(self, request, *args, **kwargs):
"LSU - s121 diversification permits",
]
)
- if (
- action == "complete"
- and task.referral.type in trigger_ref_type
- and not task.referral.has_location
- ):
+ if action == "complete" and task.referral.type in trigger_ref_type and not task.referral.has_location:
msg = """You are unable to complete this task without first
recording location(s) on the referral.
Click here to create location(s).""".format(
@@ -1356,9 +1282,7 @@ def get_context_data(self, **kwargs):
# Create a breadcrumb trail: Home[URL] > Tasks[URL] > ID[URL] > Action
action = self.kwargs["action"]
obj = self.get_object()
- context["page_title"] = "PRS | Tasks | {} | {}".format(
- obj.pk, action.capitalize()
- )
+ context["page_title"] = "PRS | Tasks | {} | {}".format(obj.pk, action.capitalize())
links = [
(reverse("site_home"), "Home"),
(reverse("prs_object_list", kwargs={"model": "tasks"}), "Tasks"),
@@ -1371,9 +1295,7 @@ def get_context_data(self, **kwargs):
context["breadcrumb_trail"] = breadcrumbs_li(links)
context["title"] = action.upper() + " TASK"
# Pass in a serialised list of tag names.
- context["tags"] = json.dumps(
- [t.name for t in Tag.objects.all().order_by("name")]
- )
+ context["tags"] = json.dumps([t.name for t in Tag.objects.all().order_by("name")])
return context
def get_success_url(self):
@@ -1430,9 +1352,7 @@ def form_valid(self, form):
elif action == "complete":
if obj.type.name == "Assess a referral":
# Rule: proposed condition is mandatory for some 'Assess' task outcomes.
- trigger_outcome = TaskState.objects.filter(
- name__in=["Response with condition"]
- )
+ trigger_outcome = TaskState.objects.filter(name__in=["Response with condition"])
trigger_ref_type = ReferralType.objects.filter(
name__in=[
"Development application",
@@ -1494,9 +1414,7 @@ def get_queryset(self):
# UserProfile referral_history is a list of lists ([pk, date]).
try: # Empty history fails.
history_list = json.loads(self.request.user.userprofile.referral_history)
- return Referral.objects.current().filter(
- pk__in=[i[0] for i in history_list]
- )
+ return Referral.objects.current().filter(pk__in=[i[0] for i in history_list])
except Exception:
return Referral.objects.none()
@@ -1544,8 +1462,7 @@ def get_context_data(self, **kwargs):
class TagList(PrsObjectList):
- """Custom view to return a readonly list of tags (rendered HTML or JSON).
- """
+ """Custom view to return a readonly list of tags (rendered HTML or JSON)."""
model = Tag
template_name = "referral/tag_list.html"
@@ -1574,16 +1491,12 @@ class TagReplace(LoginRequiredMixin, FormView):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser and not is_prs_power_user(request):
- return HttpResponseForbidden(
- "You do not have permission to use this function."
- )
+ return HttpResponseForbidden("You do not have permission to use this function.")
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context["page_title"] = " | ".join(
- [settings.APPLICATION_ACRONYM, "Replace tag"]
- )
+ context["page_title"] = " | ".join([settings.APPLICATION_ACRONYM, "Replace tag"])
context["title"] = "REPLACE TAG"
return context
@@ -1605,15 +1518,12 @@ def form_valid(self, form):
obj.tags.add(new)
# Finally, delete the old tag
old.delete()
- messages.success(
- self.request, 'All "{}" tags have been replaced by "{}"'.format(old, new)
- )
+ messages.success(self.request, 'All "{}" tags have been replaced by "{}"'.format(old, new))
return HttpResponseRedirect(reverse("tag_list"))
class ReferralTagged(PrsObjectList):
- """Override the Referral model list view to filter tagged objects.
- """
+ """Override the Referral model list view to filter tagged objects."""
model = Referral
@@ -1645,8 +1555,7 @@ def get_context_data(self, **kwargs):
class BookmarkList(PrsObjectList):
- """Override to default object list to only show current user bookmarks.
- """
+ """Override to default object list to only show current user bookmarks."""
model = Bookmark
@@ -1679,9 +1588,7 @@ def post(self, request, *args, **kwargs):
# Delete referral relationships
# We can just call delete on this queryset.
- RelatedReferral.objects.filter(
- Q(from_referral=ref) | Q(to_referral=ref)
- ).delete()
+ RelatedReferral.objects.filter(Q(from_referral=ref) | Q(to_referral=ref)).delete()
# Delete any tags on the referral
ref.tags.clear()
# Delete tasks
@@ -1719,8 +1626,7 @@ def post(self, request, *args, **kwargs):
class ReferralRelate(PrsObjectList):
- """Custom list view to search referrals to relate together.
- """
+ """Custom list view to search referrals to relate together."""
model = Referral
template_name = "referral/referral_relate.html"
@@ -1753,14 +1659,14 @@ def post(self, request, *args, **kwargs):
# NOTE: query parameters always live in request.GET.
if not self.request.GET.get("ref_pk", None):
raise AttributeError(
- "Relate view {} must be called with a "
- "ref_pk query parameter.".format(self.__class__.__name__)
+ "Relate view {} must be called with a " "ref_pk query parameter.".format(self.__class__.__name__)
)
if "create" not in self.request.GET and "delete" not in self.request.GET:
raise AttributeError(
- "Relate view {} must be called with either "
- "create or delete query parameters.".format(self.__class__.__name__)
+ "Relate view {} must be called with either " "create or delete query parameters.".format(
+ self.__class__.__name__
+ )
)
ref1 = self.get_object()
@@ -1776,9 +1682,9 @@ def post(self, request, *args, **kwargs):
# Invalidate the cached referral detail fragments.
if settings.REDIS_CACHE_HOST:
- key = make_template_fragment_key('referral_detail', [ref1.pk])
+ key = make_template_fragment_key("referral_detail", [ref1.pk])
cache.delete(key)
- key = make_template_fragment_key('referral_detail', [ref2.pk])
+ key = make_template_fragment_key("referral_detail", [ref2.pk])
cache.delete(key)
return redirect(ref1.get_absolute_url())
@@ -1833,15 +1739,11 @@ def post(self, request, *args, **kwargs):
# On Cancel, redirect to the Condition URL.
if request.POST.get("cancel"):
- return HttpResponseRedirect(
- reverse(
- "prs_object_detail", kwargs={"pk": obj.pk, "model": "conditions"}
- )
- )
+ return HttpResponseRedirect(reverse("prs_object_detail", kwargs={"pk": obj.pk, "model": "conditions"}))
# Invalidate any cached referral detail fragment.
if settings.REDIS_CACHE_HOST:
- key = make_template_fragment_key('referral_detail', [obj.referral.pk])
+ key = make_template_fragment_key("referral_detail", [obj.referral.pk])
cache.delete(key)
return super().post(request, *args, **kwargs)
@@ -1861,64 +1763,48 @@ def form_valid(self, form):
self.request.user,
)
clearance_task.save()
- obj.add_clearance(
- task=clearance_task, deposited_plan=form.cleaned_data["deposited_plan"]
- )
+ obj.add_clearance(task=clearance_task, deposited_plan=form.cleaned_data["deposited_plan"])
messages.success(self.request, "New clearance request created successfully.")
# If the user check the "Email user" box, send them a notification.
if self.request.POST.get("email_user"):
- subject = "PRS referral {0} - new clearance request notification".format(
- clearance_task.referral.pk
- )
+ subject = "PRS referral {0} - new clearance request notification".format(clearance_task.referral.pk)
from_email = self.request.user.email
to_email = clearance_task.assigned_user.email
- referral_url = (
- settings.SITE_URL + clearance_task.referral.get_absolute_url()
- )
+ referral_url = settings.SITE_URL + clearance_task.referral.get_absolute_url()
text_content = """This is an automated message to let you know that you have
been assigned PRS clearance request {0} by the sending user.\n
This clearance request is attached to referral ID {1}.\n
- """.format(
- clearance_task.pk, clearance_task.referral.pk
- )
+ """.format(clearance_task.pk, clearance_task.referral.pk)
html_content = """This is an automated message to let you know that you have
been assigned PRS clearance request {0} by the sending user.
This task is attached to referral ID {1}, at this URL:
- {2}
""".format(
- clearance_task.pk, clearance_task.referral.pk, referral_url
- )
+ {2}
""".format(clearance_task.pk, clearance_task.referral.pk, referral_url)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
msg.attach_alternative(html_content, "text/html")
# Email should fail gracefully - ie no Exception raised on failure.
msg.send(fail_silently=True)
# Business rule: for each new clearance request, email users in the PRS power users group.
- subject = "PRS referral {} - new condition clearance request notification".format(
- clearance_task.referral.pk
- )
+ subject = "PRS referral {} - new condition clearance request notification".format(clearance_task.referral.pk)
from_email = "PRS-Alerts@dbca.wa.gov.au"
pu_group = Group.objects.get(name=settings.PRS_POWER_USER_GROUP)
to_email = [user.email for user in pu_group.user_set.filter(is_active=True)]
- text_content = "This is an automated message to let you know that the following clearance request was just created:\n"
- html_content = "This is an automated message to let you know that the following clearance request was just created:
"
+ text_content = (
+ "This is an automated message to let you know that the following clearance request was just created:\n"
+ )
+ html_content = (
+ "This is an automated message to let you know that the following clearance request was just created:
"
+ )
text_content += "* Task ID {}\n".format(clearance_task.pk)
html_content += 'Task ID {}
'.format(
settings.SITE_URL + clearance_task.get_absolute_url(), clearance_task.pk
)
- text_content += "The clearance task was created by {}.\n".format(
- clearance_task.creator.get_full_name()
- )
- html_content += "The clearance task was created by {}.
".format(
- clearance_task.creator.get_full_name()
- )
- text_content += (
- "This is an automatically-generated email - please do not reply.\n"
- )
- html_content += (
- "This is an automatically-generated email - please do not reply.
"
- )
+ text_content += "The clearance task was created by {}.\n".format(clearance_task.creator.get_full_name())
+ html_content += "The clearance task was created by {}.
".format(clearance_task.creator.get_full_name())
+ text_content += "This is an automatically-generated email - please do not reply.\n"
+ html_content += "This is an automatically-generated email - please do not reply.
"
msg = EmailMultiAlternatives(subject, text_content, from_email, to_email)
msg.attach_alternative(html_content, "text/html")
# Email should fail gracefully - i.e. no Exception raised on failure.
@@ -1937,40 +1823,38 @@ def get(self, request, *args, **kwargs):
record = get_object_or_404(Record, pk=self.kwargs["pk"])
if record.infobase_id:
response = HttpResponse(content_type="application/octet-stream")
- response[
- "Content-Disposition"
- ] = "attachment; filename=infobase_{}.obr".format(record.infobase_id)
+ response["Content-Disposition"] = "attachment; filename=infobase_{}.obr".format(record.infobase_id)
# The HttpResponse is a file-like object; write the Infobase ID and return it.
response.write(record.infobase_id)
return response
else:
- messages.warning(
- request, "That record is not associated with an InfoBase object ID."
- )
+ messages.warning(request, "That record is not associated with an InfoBase object ID.")
return HttpResponseRedirect(record.get_absolute_url())
class CadastreQuery(View):
- """Basic view endpoint to send a CQL filter query to the Cadastre spatial service.
- """
+ """Basic view endpoint to send a CQL filter query to the Cadastre spatial service."""
+
http_method_names = ["get"]
def get(self, request, *args, **kwargs):
cql_filter = request.GET.get("cql_filter", None)
if not cql_filter:
+ # This view requires a CQL filter value.
return HttpResponseBadRequest("Bad request")
crs = request.GET.get("crs", None)
+ type_name = env("CADASTRE_LAYER_NAME")
if crs:
- resp = query_cadastre(cql_filter, crs)
+ resp = wfs_getfeature(type_name, cql_filter, crs)
else:
- resp = query_cadastre(cql_filter)
+ resp = wfs_getfeature(type_name, cql_filter)
return JsonResponse(resp)
class ReferralMap(LoginRequiredMixin, TemplateView):
- """A map view displaying all referral locations.
- """
+ """A map view displaying all referral locations."""
+
template_name = "referral/referral_map.html"
def get_context_data(self, **kwargs):
@@ -1980,8 +1864,8 @@ def get_context_data(self, **kwargs):
class GeocodeQuery(View):
- """Basic view endpoint to send a geocode query to the Caddy spatial service.
- """
+ """Basic view endpoint to send a geocode query to the Caddy spatial service."""
+
http_method_names = ["get"]
def get(self, request, *args, **kwargs):
diff --git a/prs2/settings.py b/prs2/settings.py
index 29cdd958..f7197eda 100644
--- a/prs2/settings.py
+++ b/prs2/settings.py
@@ -8,6 +8,7 @@
from dbca_utils.utils import env
from django.core.exceptions import DisallowedHost
from django.db.utils import OperationalError
+from redis.exceptions import ConnectionError
# Project paths
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@@ -47,9 +48,7 @@
AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME", "name")
AZURE_ACCOUNT_KEY = env("AZURE_ACCOUNT_KEY", "key")
AZURE_CONTAINER = env("AZURE_CONTAINER", "container")
- AZURE_URL_EXPIRATION_SECS = env(
- "AZURE_URL_EXPIRATION_SECS", 3600
- ) # Default one hour.
+ AZURE_URL_EXPIRATION_SECS = env("AZURE_URL_EXPIRATION_SECS", 3600) # Default one hour.
# PRS may deploy its own instance of Geoserver.
KMI_GEOSERVER_URL = env("KMI_GEOSERVER_URL", "")
@@ -298,6 +297,9 @@ def sentry_excluded_exceptions(event, hint):
# Exclude exceptions related to host requests not in ALLOWED_HOSTS.
elif hint["exc_info"][0] is DisallowedHost:
return None
+ # Exclude Redis service connection errors.
+ elif hint["exc_info"][0] is ConnectionError:
+ return None
return event
@@ -305,12 +307,8 @@ def sentry_excluded_exceptions(event, hint):
# Sentry config
SENTRY_DSN = env("SENTRY_DSN", None)
SENTRY_SAMPLE_RATE = env("SENTRY_SAMPLE_RATE", 1.0) # Error sampling rate
-SENTRY_TRANSACTION_SAMPLE_RATE = env(
- "SENTRY_TRANSACTION_SAMPLE_RATE", 0.0
-) # Transaction sampling
-SENTRY_PROFILES_SAMPLE_RATE = env(
- "SENTRY_PROFILES_SAMPLE_RATE", 0.0
-) # Proportion of sampled transactions to profile.
+SENTRY_TRANSACTION_SAMPLE_RATE = env("SENTRY_TRANSACTION_SAMPLE_RATE", 0.0) # Transaction sampling
+SENTRY_PROFILES_SAMPLE_RATE = env("SENTRY_PROFILES_SAMPLE_RATE", 0.0) # Proportion of sampled transactions to profile.
SENTRY_ENVIRONMENT = env("SENTRY_ENVIRONMENT", None)
if SENTRY_DSN and SENTRY_ENVIRONMENT:
import sentry_sdk
diff --git a/pyproject.toml b/pyproject.toml
index f12444a7..af07a4e0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,15 +8,15 @@ package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
-django = "4.2.15"
-psycopg = { version = "3.2.2", extras = ["binary", "pool"] }
+django = "4.2.16"
+psycopg = {version = "3.2.3", extras = ["binary", "pool"]}
dbca-utils = "2.0.2"
python-dotenv = "1.0.1"
dj-database-url = "2.2.0"
gunicorn = "23.0.0"
django-crispy-forms = "2.3"
django-reversion = "5.1.0"
-django-taggit = "6.0.0"
+django-taggit = "6.1.0"
unidecode = "1.3.8"
pillow = "10.4.0"
python-magic = "0.4.27"
@@ -37,7 +37,7 @@ whitenoise = { version = "6.7.0", extras = ["brotli"] }
django-crum = "0.7.9"
sentry-sdk = { version = "2.14.0", extras = ["django"] }
crispy-bootstrap5 = "2024.2"
-redis = "5.0.8"
+redis = "5.1.0"
xlsxwriter = "3.2.0"
django-storages = { version = "1.14.4", extras = ["azure"] }