diff --git a/Pipfile b/Pipfile index a617b382f6..46462176c6 100644 --- a/Pipfile +++ b/Pipfile @@ -20,7 +20,7 @@ ipdb = "*" watchdog = {extras = ["watchmedo"], version = "*"} diff-pdf-visually = "~=1.7.0" pytest-circleci-parallelized = "~=0.1.0" -moto = {extras = ["s3"], version = "*"} +moto = {extras = ["s3"], version = "==5.0.3"} [packages] factory-boy = "~=2.12.0" @@ -32,7 +32,7 @@ django-allow-cidr = "~=0.5.0" django-elasticsearch-dsl = "~=7.2.2" django-elasticsearch-dsl-drf = "~=0.22.5" django-environ = "~=0.9.0" -django-health-check = "~=3.16.5" +django-health-check = "~=3.18.1" django-model-utils = "~=4.3.1" django-sortedm2m = "~=3.1.1" django-staff-sso-client = "~=4.2.1" @@ -53,7 +53,7 @@ pypdf2 = "~=1.27.5" cryptography = "~=42.0.0" sentry-sdk = "~=1.17.0" elastic-apm = "~=6.7.2" -gunicorn = "~=21.2.0" +gunicorn = "~=22.0.0" gevent = "~=23.9.1" xmltodict = "~=0.12.0" Pillow = "~=10.2.0" @@ -66,12 +66,12 @@ django-extensions = "~=3.2.3" ipython = "~=7.34.0" celery = "~=5.3.0" redis = "~=4.4.4" -psycopg2-binary = "~=2.9.3" django-test-migrations = "~=1.2.0" django-silk = "~=5.0.3" django = "~=4.2.10" django-queryable-properties = "~=1.9.1" database-sanitizer = ">=1.1.0" +psycopg = "~=3.1.18" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index a650ed21d6..8319c83904 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7fc4fb27a0ebd9f22909e7281c0a804bec9c4036a3ad9668519e768e9f710d13" + "sha256": "88af2a9ada474205b1f2a0850c36fb5e29751000c8bf39b676efb99a017302c8" }, "pipfile-spec": 6, "requires": { @@ -475,12 +475,12 @@ }, "django-health-check": { "hashes": [ - "sha256:4f4fe32838eb367b9dda51669f128b97f8416eaa66b80b58c50db6fc2cc42356", - "sha256:85b8e4ffa6ebbee3a7214c91ea4a67ce0e918bc8ed9679d054afd9cc9fa17c4f" + "sha256:2c89a326cd79830e2fc6808823a9e7e874ab23f7aef3ff2c4d1194c998e1dca1", + "sha256:44552d55ae8950c9548d3b90f9d9fd5570b57446a19b2a8e674c82f993cb7a2c" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.16.7" + "version": "==3.18.1" }, "django-ipware": { "hashes": [ @@ -789,12 +789,12 @@ }, "gunicorn": { "hashes": [ - "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", - "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", + "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63" ], "index": "pypi", - "markers": "python_version >= '3.5'", - "version": "==21.2.0" + "markers": "python_version >= '3.7'", + "version": "==22.0.0" }, "html5lib": { "hashes": [ @@ -806,11 +806,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "importlib-metadata": { "hashes": [ @@ -854,11 +854,11 @@ }, "kombu": { "hashes": [ - "sha256:0eac1bbb464afe6fb0924b21bf79460416d25d8abc52546d4f16cad94f789488", - "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93" + "sha256:49f1e62b12369045de2662f62cc584e7df83481a513db83b01f87b5b9785e378", + "sha256:f3da5b570a147a5da8280180aa80b03807283d63ea5081fcdb510d18242431d9" ], "markers": "python_version >= '3.8'", - "version": "==5.3.5" + "version": "==5.3.6" }, "kubi-ecs-logger": { "hashes": [ @@ -942,11 +942,11 @@ }, "parso": { "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", + "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" ], "markers": "python_version >= '3.6'", - "version": "==0.8.3" + "version": "==0.8.4" }, "pexpect": { "hashes": [ @@ -958,11 +958,11 @@ }, "phonenumbers": { "hashes": [ - "sha256:991f2619f0593b36b674c345af47944ec4bae526b353cf53d707e662087be63b", - "sha256:f2d653268ece55a4f3752d9cda4be6f7465f298e6d028d522aedda13cf057201" + "sha256:7c2676be07b7d0f74411e275e0660380a0ec3ee0d359f070d719424bd2c5f62e", + "sha256:bc0bb5d3bab29e28549194f6bf57cb3ca03c3dd84238af12674fe24031631bda" ], "index": "pypi", - "version": "==8.13.33" + "version": "==8.13.34" }, "pickleshare": { "hashes": [ @@ -1054,84 +1054,14 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.0.43" }, - "psycopg2-binary": { - "hashes": [ - "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", - "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", - "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", - "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", - "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", - "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", - "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", - "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", - "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", - "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", - "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", - "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", - "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", - "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", - "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", - "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", - "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", - "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", - "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", - "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", - "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", - "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", - "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", - "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", - "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", - "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", - "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", - "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", - "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", - "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", - "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", - "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", - "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", - "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", - "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", - "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", - "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", - "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", - "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", - "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", - "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", - "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", - "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", - "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", - "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", - "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", - "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", - "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", - "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", - "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", - "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", - "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", - "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", - "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", - "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", - "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", - "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", - "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", - "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", - "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", - "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", - "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", - "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", - "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", - "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", - "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", - "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", - "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", - "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", - "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", - "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", - "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" + "psycopg": { + "hashes": [ + "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b", + "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.9.9" + "version": "==3.1.18" }, "ptyprocess": { "hashes": [ @@ -1150,10 +1080,11 @@ }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" ], - "version": "==2.21" + "markers": "python_version >= '3.8'", + "version": "==2.22" }, "pygments": { "hashes": [ @@ -1369,11 +1300,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version >= '3.8'", - "version": "==4.10.0" + "version": "==4.11.0" }, "tzdata": { "hashes": [ @@ -1396,7 +1327,7 @@ "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.26.18" }, "vine": { @@ -1911,19 +1842,19 @@ }, "gitpython": { "hashes": [ - "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd", - "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb" + "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", + "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff" ], "markers": "python_version >= '3.7'", - "version": "==3.1.42" + "version": "==3.1.43" }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "iniconfig": { "hashes": [ @@ -2012,11 +1943,11 @@ }, "mako": { "hashes": [ - "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e", - "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c" + "sha256:5324b88089a8978bf76d1629774fcc2f1c07b82acdf00f4c5dd8ceadfffc4b40", + "sha256:e16c01d9ab9c11f7290eef1cfefc093fb5a45ee4a3da09e2fec2e4d1bae54e73" ], "markers": "python_version >= '3.8'", - "version": "==1.3.2" + "version": "==1.3.3" }, "markupsafe": { "hashes": [ @@ -2152,11 +2083,11 @@ }, "parso": { "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", + "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" ], "markers": "python_version >= '3.6'", - "version": "==0.8.3" + "version": "==0.8.4" }, "pathspec": { "hashes": [ @@ -2251,10 +2182,11 @@ }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" ], - "version": "==2.21" + "markers": "python_version >= '3.8'", + "version": "==2.22" }, "pydocstyle": { "hashes": [ @@ -2434,11 +2366,12 @@ }, "requests-mock": { "hashes": [ - "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4", - "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15" + "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", + "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401" ], "index": "pypi", - "version": "==1.11.0" + "markers": "python_version >= '3.5'", + "version": "==1.12.1" }, "requirements-detector": { "hashes": [ @@ -2545,18 +2478,18 @@ }, "typing-extensions": { "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version >= '3.8'", - "version": "==4.10.0" + "version": "==4.11.0" }, "urllib3": { "hashes": [ "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.26.18" }, "watchdog": { @@ -2606,11 +2539,11 @@ }, "werkzeug": { "hashes": [ - "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc", - "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10" + "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795", + "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d" ], "markers": "python_version >= '3.8'", - "version": "==3.0.1" + "version": "==3.0.2" }, "wrapt": { "hashes": [ diff --git a/README.md b/README.md index 72fdf4deca..32849e102e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Service for handling backend calls in LITE. - Clone the repository: - `git clone https://github.com/uktrade/lite-api.git` - `cd lite-api` + - Install [Homebrew](https://brew.sh/) ### A note on running the service without Docker @@ -56,7 +57,7 @@ Service for handling backend calls in LITE. - Run the `make doc-migrate` command which does all of the above in one - Option 3: - Run `docker-compose up` to start the API's Django server - - Run the `make first-run` command which will run the migrations, seedall and populate the databse with test data + - Run the `make first-run` command which will run the migrations, seedall and populate the database with test data - Starting the service for the first time - `docker-compose up` - to start the API's Django server @@ -116,13 +117,13 @@ We currently use celery for async tasks and scheduling in LITE; To produce PDF documents you will also need to install WeasyPrint. Do this after installing the python packages in the Pipfile; -> MacOS: https://weasyprint.readthedocs.io/en/stable/install.html#macos +> MacOS: `brew install weasyprint` > Linux: https://weasyprint.readthedocs.io/en/stable/install.html#debian-ubuntu ## Installing endesive for document signing -To digitally sign documents `endesive` requires the OS library `swig` to be installed. To install run `sudo apt-get install swig` +To digitally sign documents `endesive` requires the OS library `swig` to be installed. To install run `brew install swig` A `.p12` file is also required. Please see https://uktrade.atlassian.net/wiki/spaces/ILT/pages/1390870733/PDF+Document+Signing @@ -139,6 +140,10 @@ A `.p12` file is also required. Please see https://uktrade.atlassian.net/wiki/sp ER diagrams can be viewed in docs/entity-relation-diagrams/. You'll need to install any dev [graphviz](https://graphviz.org/) dependencies (on ubuntu `sudo apt install libgraphviz-dev`) and then `pygraphviz`. + ``` + brew install graphviz + pip install pygraphviz + ``` Gegenerate diagrams diff --git a/api/cases/migrations/0064_update_teams_on_advice.py b/api/cases/migrations/0064_update_teams_on_advice.py new file mode 100644 index 0000000000..892c0792e4 --- /dev/null +++ b/api/cases/migrations/0064_update_teams_on_advice.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-04-11 14:51 + +from django.db import migrations + + +def update_teams_on_advice(apps, schema_editor): + Advice = apps.get_model("cases", "Advice") + + teams = set(Advice.objects.values_list("user__team", flat=True)) + for team in teams: + advice_without_a_team = Advice.objects.filter(team__isnull=True, user__team=team) + advice_without_a_team.update(team=team) + + +class Migration(migrations.Migration): + + dependencies = [ + ("cases", "0063_ecjuquery_chaser_email_sent_on"), + ] + + operations = [ + migrations.RunPython( + update_teams_on_advice, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/api/cases/tests/test_migrations.py b/api/cases/tests/test_migrations.py new file mode 100644 index 0000000000..4751e7d97e --- /dev/null +++ b/api/cases/tests/test_migrations.py @@ -0,0 +1,78 @@ +from django_test_migrations.contrib.unittest_case import MigratorTestCase + +from api.core.constants import Roles + + +class TestAdviceTeamMigration(MigratorTestCase): + migrate_from = ("cases", "0063_ecjuquery_chaser_email_sent_on") + migrate_to = ("cases", "0064_update_teams_on_advice") + + def prepare(self): + Role = self.old_state.apps.get_model("users", "Role") + Role.objects.create(id=Roles.INTERNAL_DEFAULT_ROLE_ID) + + Team = self.old_state.apps.get_model("teams", "Team") + self.a_team = Team.objects.create(name="a_team") + self.b_team = Team.objects.create(name="b_team") + + BaseUser = self.old_state.apps.get_model("users", "BaseUser") + a_team_base_user = BaseUser.objects.create(email="a_team_base_user@example.com") # /PS-IGNORE + b_team_base_user = BaseUser.objects.create(email="b_team_base_user@example.com") # /PS-IGNORE + + GovUser = self.old_state.apps.get_model("users", "GovUser") + a_team_user = GovUser.objects.create( + baseuser_ptr=a_team_base_user, + team=self.a_team, + ) + b_team_user = GovUser.objects.create( + baseuser_ptr=b_team_base_user, + team=self.b_team, + ) + + Organisation = self.old_state.apps.get_model("organisations", "Organisation") + organisation = Organisation.objects.create(name="test") + + CaseType = self.old_state.apps.get_model("cases", "CaseType") + case_type = CaseType.objects.get(pk="00000000-0000-0000-0000-000000000004") + + Case = self.old_state.apps.get_model("cases", "Case") + case = Case.objects.create( + case_type=case_type, + organisation=organisation, + ) + + Advice = self.old_state.apps.get_model("cases", "Advice") + self.user_advice_with_same_team = Advice.objects.create( + case=case, + team=self.a_team, + user=a_team_user, + ) + self.user_advice_with_different_team = Advice.objects.create( + case=case, + team=self.a_team, + user=b_team_user, + ) + self.user_advice_without_team = Advice.objects.create( + case=case, + team=None, + user=a_team_user, + ) + + def test_migration_0064_update_teams_on_advice(self): + self.user_advice_with_same_team.refresh_from_db() + self.assertEqual( + self.user_advice_with_same_team.team, + self.a_team, + ) + + self.user_advice_with_different_team.refresh_from_db() + self.assertEqual( + self.user_advice_with_different_team.team, + self.a_team, + ) + + self.user_advice_without_team.refresh_from_db() + self.assertEqual( + self.user_advice_without_team.team, + self.a_team, + ) diff --git a/api/document_data/tests/test_celery_tasks.py b/api/document_data/tests/test_celery_tasks.py index 719ebce46f..35f40e6259 100644 --- a/api/document_data/tests/test_celery_tasks.py +++ b/api/document_data/tests/test_celery_tasks.py @@ -45,7 +45,7 @@ def test_backup_new_document_data(self): "thisisakey", ) self.assertEqual( - document_data.data.tobytes(), + document_data.data, b"test", ) s3_object = self.get_object_from_default_bucket("thisisakey") @@ -92,7 +92,7 @@ def test_update_existing_document_data(self): "thisisakey", ) self.assertEqual( - document_data.data.tobytes(), + document_data.data, b"new contents", ) s3_object = self.get_object_from_default_bucket("thisisakey") @@ -139,7 +139,7 @@ def test_leave_existing_document_data(self): "thisisakey", ) self.assertEqual( - document_data.data.tobytes(), + document_data.data, b"test", ) self.assertEqual( diff --git a/api/external_data/enums.py b/api/external_data/enums.py index 515944be29..13033c0c7c 100644 --- a/api/external_data/enums.py +++ b/api/external_data/enums.py @@ -6,3 +6,11 @@ class DenialMatchCategory: (PARTIAL, "Partial"), (EXACT, "Exact"), ] + + +class DenialEntityType: + CONSIGNEE = "consignee" + END_USER = "end_user" + THIRD_PARTY = "third_party" + + choices = ((CONSIGNEE, "Consignee"), (END_USER, "End-user"), (THIRD_PARTY, "Third party")) diff --git a/api/external_data/management/commands/ingest_denials.py b/api/external_data/management/commands/ingest_denials.py index 5c92fa16f9..344c1b601e 100644 --- a/api/external_data/management/commands/ingest_denials.py +++ b/api/external_data/management/commands/ingest_denials.py @@ -5,13 +5,13 @@ from django.core.management.base import BaseCommand from django.db import transaction from api.applications.models import DenialMatchOnApplication +from api.external_data.serializers import DenialEntitySerializer from rest_framework import serializers from elasticsearch_dsl import connections from api.documents.libraries import s3_operations from api.external_data import documents -from api.external_data.serializers import DenialEntitySerializer from api.external_data.models import DenialEntity @@ -63,15 +63,22 @@ def handle(self, *args, **options): @transaction.atomic def load_denials(self, filename): - data = get_json_content_and_delete(filename) + errors = [] if data: # Lets delete all denial records except ones that have been matched matched_denial_ids = DenialMatchOnApplication.objects.all().values_list("denial_id", flat=True).distinct() DenialEntity.objects.all().exclude(id__in=matched_denial_ids).delete() - errors = [] for i, row in enumerate(data, start=1): + # This is required so we don't reload the same denial entity and load duplicates + has_fields = bool(row.get("regime_reg_ref") and row.get("name")) + if has_fields: + exists = DenialMatchOnApplication.objects.filter( + denial__regime_reg_ref=row["regime_reg_ref"], denial__name=row["name"] + ).exists() + if exists: + continue serializer = DenialEntitySerializer( data={ "data": row, diff --git a/api/external_data/management/commands/tests/test_ingest_denials.py b/api/external_data/management/commands/tests/test_ingest_denials.py index 7737092cc8..fe890bf1a8 100644 --- a/api/external_data/management/commands/tests/test_ingest_denials.py +++ b/api/external_data/management/commands/tests/test_ingest_denials.py @@ -127,12 +127,13 @@ def test_populate_denials_validation_call(mock_json_content, mock_delete_file): def test_populate_denials_with_existing_matching_records(mock_get_file, mock_delete_file, json_file_data): mock_get_file.return_value = json_file_data case = StandardApplicationFactory() - denial = DenialMatchFactory() - DenialMatchOnApplicationFactory(application=case, category="exact", denial=denial) + + denial_enity = DenialMatchFactory(regime_reg_ref="12", name="Test1 case") + DenialMatchOnApplicationFactory(application=case, category="exact", denial=denial_enity) call_command("ingest_denials", "json_file") - assert DenialEntity.objects.all().count() == 4 + assert DenialEntity.objects.all().count() == 3 @pytest.mark.django_db diff --git a/api/external_data/migrations/0022_denialentity_entity_type.py b/api/external_data/migrations/0022_denialentity_entity_type.py new file mode 100644 index 0000000000..488f631852 --- /dev/null +++ b/api/external_data/migrations/0022_denialentity_entity_type.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.11 on 2024-04-18 08:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("external_data", "0021_denialentity_denial_id"), + ] + + operations = [ + migrations.AddField( + model_name="denialentity", + name="entity_type", + field=models.TextField( + blank=True, + choices=[("consignee", "Consignee"), ("end_user", "End-user"), ("third_party", "Third party")], + default="", + help_text="Type of entity being denied", + null=True, + ), + ), + ] diff --git a/api/external_data/models.py b/api/external_data/models.py index f2aa5ab4aa..4286bdeafd 100644 --- a/api/external_data/models.py +++ b/api/external_data/models.py @@ -5,6 +5,7 @@ from api.common.models import TimestampableModel from api.flags.models import Flag from api.users.models import GovUser +from api.external_data.enums import DenialEntityType class Denial(TimestampableModel): @@ -59,6 +60,9 @@ class DenialEntity(TimestampableModel): help_text="Reason why the denial was refused", blank=True, default="", null=True ) spire_entity_id = models.IntegerField(help_text="Entity_id from spire for matching data", null=True) + entity_type = models.TextField( + choices=DenialEntityType.choices, help_text="Type of entity being denied", blank=True, default="", null=True + ) class SanctionMatch(TimestampableModel): diff --git a/api/letter_templates/context_generator.py b/api/letter_templates/context_generator.py index a7c529c38d..f6579a8206 100644 --- a/api/letter_templates/context_generator.py +++ b/api/letter_templates/context_generator.py @@ -885,11 +885,14 @@ def _get_goods_context(application, final_advice, licence=None): # Ensure that for each proviso final advice record, we add a record to the goods # context for advice in final_advice: - if advice.good_id in good_ids_to_goods_on_application: - # Grab the next GoodOnApplication for this Good.id - this ensures that - # each GoodOnApplication is present once on the end licence - good_on_application = good_ids_to_goods_on_application[advice.good_id].pop(0) - goods_context[advice.type].append(_get_good_on_application_context_with_advice(good_on_application, advice)) + # Ignore final advice records where we have no associated good on application + # - either our mapping value is missing or is an empty list so skip it. + if not good_ids_to_goods_on_application.get(advice.good_id): + continue + # Grab the next GoodOnApplication for this Good.id - this ensures that + # each GoodOnApplication is present once on the end licence + good_on_application = good_ids_to_goods_on_application[advice.good_id].pop(0) + goods_context[advice.type].append(_get_good_on_application_context_with_advice(good_on_application, advice)) # Because we append goods that are approved with proviso to the approved goods below # we need to make sure only to keep approved goods that are not in proviso goods diff --git a/api/letter_templates/tests/test_context_generation.py b/api/letter_templates/tests/test_context_generation.py index 09799deffe..16f8b26be1 100644 --- a/api/letter_templates/tests/test_context_generation.py +++ b/api/letter_templates/tests/test_context_generation.py @@ -492,6 +492,43 @@ def test_generate_context_with_advice_on_goods_missing(self): self.assertEqual(context["case_officer_name"], case.get_case_officer_name()) self._assert_good_with_advice(context["goods"], final_advice, case.goods.all()[0]) + def test_generate_context_with_advice_on_goods_some_missing(self): + """ + This tests the scenario where advice had been given on for two + GoodOnApplications referring to the same Good. After application editing, + only one GoodOnApplication remains despite both advice records still being present. + """ + case = self.create_standard_application_case(self.organisation, user=self.exporter_user) + good = case.goods.first().good + another_good_on_application = GoodOnApplicationFactory( + application=case, + good=good, + quantity=10, + unit=Units.NAR, + value=500, + ) + final_advice = FinalAdviceFactory( + user=self.gov_user, + case=case, + type=AdviceType.REFUSE, + good=good, + ) + FinalAdviceFactory( + user=self.gov_user, + case=case, + type=AdviceType.REFUSE, + good=good, + ) + + case.goods.first().delete() # Remove the first good from the application + + context = get_document_context(case) + render_to_string(template_name="letter_templates/case_context_test.html", context=context) + + self.assertEqual(context["case_reference"], case.reference_code) + self.assertEqual(context["case_officer_name"], case.get_case_officer_name()) + self._assert_good_with_advice(context["goods"], final_advice, case.goods.all()[0]) + def test_generate_context_with_proviso_advice_on_goods(self): case = self.create_standard_application_case(self.organisation, user=self.exporter_user) good = case.goods.first()