Merge remote-tracking branch 'origin/main' into rjm/998-domain-growth-export

This commit is contained in:
Rachid Mrad 2023-12-27 15:25:43 -05:00
commit 9e7802c349
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
40 changed files with 1522 additions and 919 deletions

View file

@ -13,7 +13,9 @@
It is necessary to use `bash -c` because `run pipenv requirements` will not recognize that it is running non-interactively and will include garbage formatting characters.
The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository.
3. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies.
3. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt.
This is done by either saving what it was originally or opening a PR and using that as a reference to undo changes to any mention of geventconnpool.
Geventconnpool, when set as a requirement without the reference portion, is defaulting to get a commit from 2014 which then breaks the code, as we want the newest version from them.
4. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies.
The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less.

499
src/Pipfile.lock generated
View file

@ -32,20 +32,20 @@
},
"boto3": {
"hashes": [
"sha256:02ce7dcad2d3b054cd99e7ca6df7a708e016a31b1c98b46d8df3b3891070c121",
"sha256:b8acb57a124434284d6ab69c61d32d70e84e13e2c27c33b4ad3c32f15ad407d3"
"sha256:d12467fb3a64d359b0bda0570a8163a5859fcac13e786f2a3db0392523178556",
"sha256:eed0f7df91066b6ac63a53d16459ac082458d57061bedf766135d9e1c2b75a6b"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.28.79"
"version": "==1.33.7"
},
"botocore": {
"hashes": [
"sha256:07ecb93833475dde68e5c0e02a7ccf8ca22caf68cdc892651c300529894133e1",
"sha256:6f1fc49e9e12f9772b4fef577837670bc84d772a7c946b4d08fe2890e34a4305"
"sha256:71ec0e85b996cf9def3dd8f4ca6cb4a9fd3a614aa4c9c7cbf33f2f68e1d0649a",
"sha256:b2299bc13bb8c0928edc98bf4594deb14cba2357536120f63772027a16ce7374"
],
"markers": "python_version >= '3.7'",
"version": "==1.31.79"
"version": "==1.33.7"
},
"cachetools": {
"hashes": [
@ -58,11 +58,11 @@
},
"certifi": {
"hashes": [
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
"sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1",
"sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"
],
"markers": "python_version >= '3.6'",
"version": "==2023.7.22"
"version": "==2023.11.17"
},
"cfenv": {
"hashes": [
@ -228,32 +228,32 @@
},
"cryptography": {
"hashes": [
"sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf",
"sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84",
"sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e",
"sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8",
"sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7",
"sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1",
"sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88",
"sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86",
"sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179",
"sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81",
"sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20",
"sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548",
"sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d",
"sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d",
"sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5",
"sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1",
"sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147",
"sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936",
"sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797",
"sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696",
"sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72",
"sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da",
"sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"
"sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960",
"sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a",
"sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc",
"sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a",
"sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf",
"sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1",
"sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39",
"sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406",
"sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a",
"sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a",
"sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c",
"sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be",
"sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15",
"sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2",
"sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d",
"sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157",
"sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003",
"sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248",
"sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a",
"sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec",
"sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309",
"sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7",
"sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"
],
"markers": "python_version >= '3.7'",
"version": "==41.0.5"
"version": "==41.0.7"
},
"defusedxml": {
"hashes": [
@ -305,19 +305,19 @@
},
"django-cache-url": {
"hashes": [
"sha256:5ca4760b4580b80e41279bc60d1e5c16a822e4e462265faab0a330701bb0ef9a",
"sha256:ef2cfacea361ee22e9b67d6ca941db22e0a9eaf892b67ca71cad52c62a17fd36"
"sha256:5f350759978483ab85dc0e3e17b3d53eed3394a28148f6bf0f53d11d0feb5b3c",
"sha256:eb9fb194717524348c95cad9905b70b647452741c1d9e481fac6d2125f0ad917"
],
"version": "==3.4.4"
"version": "==3.4.5"
},
"django-cors-headers": {
"hashes": [
"sha256:25aabc94d4837678c1edf442c7f68a5f5fd151f6767b0e0b01c61a2179d02711",
"sha256:bd36c7aea0d070e462f3383f0dc9ef717e5fdc2b10a99c98c285f16da84ffba2"
"sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36",
"sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.3.0"
"version": "==4.3.1"
},
"django-csp": {
"hashes": [
@ -375,11 +375,11 @@
},
"faker": {
"hashes": [
"sha256:14ccb0aec342d33aa3889a864a56e5b3c2d56bce1b89f9189f4fbc128b9afc1e",
"sha256:da880a76322db7a879c848a0771e129338e0a680a9f695fd9a3e7a6ac82b45e1"
"sha256:562a3a09c3ed3a1a7b20e13d79f904dfdfc5e740f72813ecf95e4cf71e5a2f52",
"sha256:aeb3e26742863d1e387f9d156f1c36e14af63bf5e6f36fb39b8c27f6a903be38"
],
"markers": "python_version >= '3.8'",
"version": "==19.13.0"
"version": "==20.1.0"
},
"fred-epplib": {
"git": "https://github.com/cisagov/epplib.git",
@ -525,11 +525,11 @@
},
"idna": {
"hashes": [
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
],
"markers": "python_version >= '3.5'",
"version": "==3.4"
"version": "==3.6"
},
"jmespath": {
"hashes": [
@ -639,11 +639,11 @@
},
"mako": {
"hashes": [
"sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818",
"sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"
"sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9",
"sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"
],
"markers": "python_version >= '3.7'",
"version": "==1.2.4"
"markers": "python_version >= '3.8'",
"version": "==1.3.0"
},
"markupsafe": {
"hashes": [
@ -745,10 +745,10 @@
},
"phonenumberslite": {
"hashes": [
"sha256:1e03f7076ab2f010088b1b8041ebdc42acd3b797e8f45997ab1861cdaea76851",
"sha256:adce353ee15b75f2deccf0eff77bada2a3d036f49ccfb30b8c172dd814fd51e9"
"sha256:305736b1b489e2bc6831710a2f34a9324f2bf96a1e77c8e0b3136dfaf9ca7753",
"sha256:6356f2728fa1d2c2bc9e79c3bfcfedc91a36537df7a134f150731a821a469a96"
],
"version": "==8.13.24"
"version": "==8.13.26"
},
"psycopg2-binary": {
"hashes": [
@ -877,131 +877,130 @@
},
"pydantic": {
"hashes": [
"sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7",
"sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"
"sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0",
"sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"
],
"markers": "python_version >= '3.7'",
"version": "==2.4.2"
"version": "==2.5.2"
},
"pydantic-core": {
"hashes": [
"sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e",
"sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33",
"sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7",
"sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7",
"sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea",
"sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4",
"sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0",
"sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7",
"sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94",
"sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff",
"sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82",
"sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd",
"sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893",
"sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e",
"sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d",
"sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901",
"sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9",
"sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c",
"sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7",
"sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891",
"sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f",
"sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a",
"sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9",
"sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5",
"sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e",
"sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a",
"sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c",
"sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f",
"sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514",
"sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b",
"sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302",
"sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096",
"sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0",
"sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27",
"sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884",
"sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a",
"sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357",
"sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430",
"sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221",
"sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325",
"sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4",
"sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05",
"sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55",
"sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875",
"sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970",
"sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc",
"sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6",
"sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f",
"sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b",
"sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d",
"sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15",
"sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118",
"sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee",
"sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e",
"sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6",
"sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208",
"sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede",
"sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3",
"sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e",
"sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada",
"sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175",
"sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a",
"sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c",
"sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f",
"sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58",
"sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f",
"sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a",
"sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a",
"sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921",
"sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e",
"sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904",
"sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776",
"sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52",
"sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf",
"sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8",
"sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f",
"sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b",
"sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63",
"sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c",
"sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f",
"sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468",
"sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e",
"sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab",
"sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2",
"sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb",
"sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb",
"sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132",
"sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b",
"sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607",
"sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934",
"sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698",
"sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e",
"sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561",
"sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de",
"sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b",
"sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a",
"sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595",
"sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402",
"sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881",
"sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429",
"sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5",
"sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7",
"sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c",
"sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531",
"sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6",
"sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"
"sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b",
"sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b",
"sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d",
"sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8",
"sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124",
"sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189",
"sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c",
"sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d",
"sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f",
"sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520",
"sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4",
"sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6",
"sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955",
"sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3",
"sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b",
"sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a",
"sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68",
"sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3",
"sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd",
"sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de",
"sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b",
"sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634",
"sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7",
"sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459",
"sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7",
"sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3",
"sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331",
"sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf",
"sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d",
"sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36",
"sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59",
"sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937",
"sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc",
"sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093",
"sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753",
"sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706",
"sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca",
"sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260",
"sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997",
"sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588",
"sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71",
"sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb",
"sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e",
"sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69",
"sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5",
"sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07",
"sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1",
"sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0",
"sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd",
"sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8",
"sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944",
"sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26",
"sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda",
"sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4",
"sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9",
"sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00",
"sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe",
"sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6",
"sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada",
"sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4",
"sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7",
"sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325",
"sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4",
"sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b",
"sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88",
"sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04",
"sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863",
"sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0",
"sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911",
"sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b",
"sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e",
"sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144",
"sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5",
"sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720",
"sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab",
"sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d",
"sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789",
"sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec",
"sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2",
"sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db",
"sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f",
"sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef",
"sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3",
"sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209",
"sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc",
"sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651",
"sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8",
"sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e",
"sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66",
"sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7",
"sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550",
"sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd",
"sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405",
"sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27",
"sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093",
"sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077",
"sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113",
"sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3",
"sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6",
"sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf",
"sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed",
"sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88",
"sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe",
"sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18",
"sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"
],
"markers": "python_version >= '3.7'",
"version": "==2.10.1"
"version": "==2.14.5"
},
"pydantic-settings": {
"hashes": [
"sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945",
"sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"
"sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c",
"sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.3"
"markers": "python_version >= '3.8'",
"version": "==2.1.0"
},
"pyjwkest": {
"hashes": [
@ -1037,19 +1036,19 @@
},
"s3transfer": {
"hashes": [
"sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a",
"sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"
"sha256:368ac6876a9e9ed91f6bc86581e319be08188dc60d50e0d56308ed5765446283",
"sha256:c9e56cbe88b28d8e197cf841f1f0c130f246595e77ae5b5a05b69fe7cb83de76"
],
"markers": "python_version >= '3.7'",
"version": "==0.7.0"
"version": "==0.8.2"
},
"setuptools": {
"hashes": [
"sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
"sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
"sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2",
"sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"
],
"markers": "python_version >= '3.8'",
"version": "==68.2.2"
"version": "==69.0.2"
},
"six": {
"hashes": [
@ -1172,28 +1171,28 @@
},
"black": {
"hashes": [
"sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884",
"sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916",
"sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258",
"sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1",
"sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce",
"sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d",
"sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982",
"sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7",
"sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173",
"sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9",
"sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb",
"sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad",
"sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc",
"sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0",
"sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a",
"sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe",
"sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace",
"sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"
"sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4",
"sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b",
"sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f",
"sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07",
"sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187",
"sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6",
"sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05",
"sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06",
"sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e",
"sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5",
"sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244",
"sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f",
"sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221",
"sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055",
"sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479",
"sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394",
"sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911",
"sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==23.10.1"
"version": "==23.11.0"
},
"blinker": {
"hashes": [
@ -1205,12 +1204,12 @@
},
"boto3": {
"hashes": [
"sha256:02ce7dcad2d3b054cd99e7ca6df7a708e016a31b1c98b46d8df3b3891070c121",
"sha256:b8acb57a124434284d6ab69c61d32d70e84e13e2c27c33b4ad3c32f15ad407d3"
"sha256:d12467fb3a64d359b0bda0570a8163a5859fcac13e786f2a3db0392523178556",
"sha256:eed0f7df91066b6ac63a53d16459ac082458d57061bedf766135d9e1c2b75a6b"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.28.79"
"version": "==1.33.7"
},
"boto3-mocking": {
"hashes": [
@ -1223,28 +1222,28 @@
},
"boto3-stubs": {
"hashes": [
"sha256:621e229ef9b394cd1f6cd5caa58a17347440b14423b01435d9f2a50031a427fc",
"sha256:f5986d1b09d516f58780100a3a86bfa75114370dd5dd0bdea67bfe8cda255723"
"sha256:0461f6fec92d96aa2ea3a207329bd020a62a7aaa86f284e5cf054d9b0c7f03c2",
"sha256:449b91060cd953e08980d76a3b67d7eb4246e663b37ecba4ec625b54619e1c22"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.28.79"
"version": "==1.33.7"
},
"botocore": {
"hashes": [
"sha256:07ecb93833475dde68e5c0e02a7ccf8ca22caf68cdc892651c300529894133e1",
"sha256:6f1fc49e9e12f9772b4fef577837670bc84d772a7c946b4d08fe2890e34a4305"
"sha256:71ec0e85b996cf9def3dd8f4ca6cb4a9fd3a614aa4c9c7cbf33f2f68e1d0649a",
"sha256:b2299bc13bb8c0928edc98bf4594deb14cba2357536120f63772027a16ce7374"
],
"markers": "python_version >= '3.7'",
"version": "==1.31.79"
"version": "==1.33.7"
},
"botocore-stubs": {
"hashes": [
"sha256:64488b9f38905f8a60041998f9dc945754222d900a3345b449059667890c2c17",
"sha256:e4d8e782d774f45dbfc36d922a0a0edfffbacca2ce66bccaba02a893a38359f2"
"sha256:ca5de1ad4dc384f919387bb96eececb70900fda9219acfcf4b473b35a4834ec9",
"sha256:f73e4728a4a391f0407cd9403f6935343aac5687fb1e0eab7c3351d3419e853b"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==1.31.79"
"version": "==1.33.7"
},
"click": {
"hashes": [
@ -1363,37 +1362,37 @@
},
"mypy": {
"hashes": [
"sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7",
"sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e",
"sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c",
"sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169",
"sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208",
"sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0",
"sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1",
"sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1",
"sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7",
"sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45",
"sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143",
"sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5",
"sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f",
"sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd",
"sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245",
"sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f",
"sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332",
"sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30",
"sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183",
"sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f",
"sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85",
"sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46",
"sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71",
"sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660",
"sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb",
"sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c",
"sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"
"sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340",
"sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49",
"sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82",
"sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce",
"sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb",
"sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51",
"sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5",
"sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e",
"sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7",
"sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33",
"sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9",
"sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1",
"sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6",
"sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a",
"sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe",
"sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7",
"sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200",
"sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7",
"sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a",
"sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28",
"sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea",
"sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120",
"sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d",
"sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42",
"sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea",
"sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2",
"sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.6.1"
"version": "==1.7.1"
},
"mypy-extensions": {
"hashes": [
@ -1437,11 +1436,11 @@
},
"platformdirs": {
"hashes": [
"sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
"sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
"sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380",
"sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"
],
"markers": "python_version >= '3.7'",
"version": "==3.11.0"
"markers": "python_version >= '3.8'",
"version": "==4.1.0"
},
"pycodestyle": {
"hashes": [
@ -1461,11 +1460,11 @@
},
"pygments": {
"hashes": [
"sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
"sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
"sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c",
"sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"
],
"markers": "python_version >= '3.7'",
"version": "==2.16.1"
"version": "==2.17.2"
},
"python-dateutil": {
"hashes": [
@ -1533,19 +1532,19 @@
},
"rich": {
"hashes": [
"sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245",
"sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"
"sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa",
"sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==13.6.0"
"version": "==13.7.0"
},
"s3transfer": {
"hashes": [
"sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a",
"sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"
"sha256:368ac6876a9e9ed91f6bc86581e319be08188dc60d50e0d56308ed5765446283",
"sha256:c9e56cbe88b28d8e197cf841f1f0c130f246595e77ae5b5a05b69fe7cb83de76"
],
"markers": "python_version >= '3.7'",
"version": "==0.7.0"
"version": "==0.8.2"
},
"six": {
"hashes": [
@ -1597,11 +1596,11 @@
},
"types-awscrt": {
"hashes": [
"sha256:4eb4f3bd0c41a2710cacda13098374da9faa76c5a0fb901aa5659e0fd48ceda1",
"sha256:a2d534b7017c3476ee69a44bd8aeaf3b588c42baa8322473d100a45ee67510d7"
"sha256:850d5ad95d8f337b15fb154790f39af077faf5c08d43758fd750f379a87d5f73",
"sha256:a577c4d60a7fb7e21b436a73207a66f6ba50329d578b347934c5d99d4d612901"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.19.8"
"version": "==0.19.19"
},
"types-cachetools": {
"hashes": [
@ -1637,11 +1636,11 @@
},
"types-s3transfer": {
"hashes": [
"sha256:aca0f2486d0a3a5037cd5b8f3e20a4522a29579a8dd183281ff0aa1c4e2c8aa7",
"sha256:ae9ed9273465d9f43da8b96307383da410c6b59c3b2464c88d20b578768e97c6"
"sha256:2e41756fcf94775a9949afa856489ac4570308609b0493dfbd7b4d333eb423e6",
"sha256:5e084ebcf2704281c71b19d5da6e1544b50859367d034b50080d5316a76a9418"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.7.0"
"version": "==0.8.2"
},
"typing-extensions": {
"hashes": [

View file

@ -1,5 +1,6 @@
import logging
from django import forms
from django.db.models.functions import Concat
from django.http import HttpResponse
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions
@ -11,7 +12,7 @@ from django.http.response import HttpResponseRedirect
from django.urls import reverse
from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.domain import Domain
from registrar.models.utility.admin_sort_fields import AdminSortFields
from registrar.models.user import User
from registrar.utility import csv_export
from . import models
from auditlog.models import LogEntry # type: ignore
@ -45,7 +46,44 @@ class CustomLogEntryAdmin(LogEntryAdmin):
add_form_template = "admin/change_form_no_submit.html"
class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
class AdminSortFields:
def get_queryset(db_field):
"""This is a helper function for formfield_for_manytomany and formfield_for_foreignkey"""
# customize sorting
if db_field.name in (
"other_contacts",
"authorizing_official",
"submitter",
):
# Sort contacts by first_name, then last_name, then email
return models.Contact.objects.all().order_by(Concat("first_name", "last_name", "email"))
elif db_field.name in ("current_websites", "alternative_domains"):
# sort web sites
return models.Website.objects.all().order_by("website")
elif db_field.name in (
"creator",
"user",
"investigator",
):
# Sort users by first_name, then last_name, then email
return models.User.objects.all().order_by(Concat("first_name", "last_name", "email"))
elif db_field.name in (
"domain",
"approved_domain",
):
# Sort domains by name
return models.Domain.objects.all().order_by("name")
elif db_field.name in ("requested_domain",):
# Sort draft domains by name
return models.DraftDomain.objects.all().order_by("name")
elif db_field.name in ("domain_application",):
# Sort domain applications by name
return models.DomainApplication.objects.all().order_by("requested_domain__name")
else:
return None
class AuditedAdmin(admin.ModelAdmin):
"""Custom admin to make auditing easier."""
def history_view(self, request, object_id, extra_context=None):
@ -58,10 +96,27 @@ class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
)
)
def formfield_for_manytomany(self, db_field, request, **kwargs):
"""customize the behavior of formfields with manytomany relationships. the customized
behavior includes sorting of objects in lists as well as customizing helper text"""
queryset = AdminSortFields.get_queryset(db_field)
if queryset:
kwargs["queryset"] = queryset
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
# customize the help text for all formfields for manytomany
formfield.help_text = (
formfield.help_text
+ " If more than one value is selected, the change/delete/view actions will be disabled."
)
return formfield
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Used to sort dropdown fields alphabetically but can be expanded upon"""
form_field = super().formfield_for_foreignkey(db_field, request, **kwargs)
return self.form_field_order_helper(form_field, db_field)
"""customize the behavior of formfields with foreign key relationships. this will customize
the behavior of selects. customized behavior includes sorting of objects in list"""
queryset = AdminSortFields.get_queryset(db_field)
if queryset:
kwargs["queryset"] = queryset
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class ListHeaderAdmin(AuditedAdmin):
@ -118,15 +173,6 @@ class ListHeaderAdmin(AuditedAdmin):
)
return filters
# customize the help_text for all formfields for manytomany
def formfield_for_manytomany(self, db_field, request, **kwargs):
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
formfield.help_text = (
formfield.help_text
+ " If more than one value is selected, the change/delete/view actions will be disabled."
)
return formfield
class UserContactInline(admin.StackedInline):
"""Edit a user's profile on the user page."""
@ -221,6 +267,10 @@ class MyUserAdmin(BaseUserAdmin):
"groups",
)
# this ordering effects the ordering of results
# in autocomplete_fields for user
ordering = ["first_name", "last_name", "email"]
# Let's define First group
# (which should in theory be the ONLY group)
def group(self, obj):
@ -489,12 +539,8 @@ class DomainInformationAdmin(ListHeaderAdmin):
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
# lists in filter_horizontal are not sorted properly, sort them
# by first_name
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name in ("other_contacts",):
kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts
return super().formfield_for_manytomany(db_field, request, **kwargs)
# Table ordering
ordering = ["domain__name"]
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
@ -547,6 +593,27 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"""Custom domain applications admin class."""
class InvestigatorFilter(admin.SimpleListFilter):
"""Custom investigator filter that only displays users with the manager role"""
title = "investigator"
# Match the old param name to avoid unnecessary refactoring
parameter_name = "investigator__id__exact"
def lookups(self, request, model_admin):
"""Lookup reimplementation, gets users of is_staff.
Returns a list of tuples consisting of (user.id, user)
"""
privileged_users = User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")
return [(user.id, user) for user in privileged_users]
def queryset(self, request, queryset):
"""Custom queryset implementation, filters by investigator"""
if self.value() is None:
return queryset
else:
return queryset.filter(investigator__id__exact=self.value())
# Columns
list_display = [
"requested_domain",
@ -558,7 +625,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
]
# Filters
list_filter = ("status", "organization_type", "investigator")
list_filter = ("status", "organization_type", InvestigatorFilter)
# Search
search_fields = [
@ -634,6 +701,9 @@ class DomainApplicationAdmin(ListHeaderAdmin):
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
# Table ordering
ordering = ["requested_domain__name"]
# lists in filter_horizontal are not sorted properly, sort them
# by website
def formfield_for_manytomany(self, db_field, request, **kwargs):
@ -641,6 +711,13 @@ class DomainApplicationAdmin(ListHeaderAdmin):
kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites
return super().formfield_for_manytomany(db_field, request, **kwargs)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
# Removes invalid investigator options from the investigator dropdown
if db_field.name == "investigator":
kwargs["queryset"] = User.objects.filter(is_staff=True)
return db_field.formfield(**kwargs)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):
if obj and obj.creator.status != models.User.RESTRICTED:
@ -774,12 +851,27 @@ class DomainInformationInline(admin.StackedInline):
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
# lists in filter_horizontal are not sorted properly, sort them
# by first_name
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name in ("other_contacts",):
kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts
return super().formfield_for_manytomany(db_field, request, **kwargs)
"""customize the behavior of formfields with manytomany relationships. the customized
behavior includes sorting of objects in lists as well as customizing helper text"""
queryset = AdminSortFields.get_queryset(db_field)
if queryset:
kwargs["queryset"] = queryset
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
# customize the help text for all formfields for manytomany
formfield.help_text = (
formfield.help_text
+ " If more than one value is selected, the change/delete/view actions will be disabled."
)
return formfield
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""customize the behavior of formfields with foreign key relationships. this will customize
the behavior of selects. customized behavior includes sorting of objects in list"""
queryset = AdminSortFields.get_queryset(db_field)
if queryset:
kwargs["queryset"] = queryset
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def get_readonly_fields(self, request, obj=None):
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
@ -801,6 +893,10 @@ class DomainAdmin(ListHeaderAdmin):
"expiration_date",
]
# this ordering effects the ordering of results
# in autocomplete_fields for domain
ordering = ["name"]
def organization_type(self, obj):
return obj.domain_info.get_organization_type_display()
@ -815,6 +911,9 @@ class DomainAdmin(ListHeaderAdmin):
change_list_template = "django/admin/domain_change_list.html"
readonly_fields = ["state", "expiration_date", "first_ready_at", "deleted_at"]
# Table ordering
ordering = ["name"]
def export_data_type(self, request):
# match the CSV example with all the fields
response = HttpResponse(content_type="text/csv")

View file

@ -329,10 +329,6 @@ class AuthorizingOfficialForm(RegistrarForm):
label="First name / given name",
error_messages={"required": ("Enter the first name / given name of your authorizing official.")},
)
middle_name = forms.CharField(
required=False,
label="Middle name (optional)",
)
last_name = forms.CharField(
label="Last name / family name",
error_messages={"required": ("Enter the last name / family name of your authorizing official.")},
@ -350,10 +346,6 @@ class AuthorizingOfficialForm(RegistrarForm):
label="Email",
error_messages={"invalid": ("Enter an email address in the required format, like name@example.com.")},
)
phone = PhoneNumberField(
label="Phone",
error_messages={"required": "Enter the phone number for your authorizing official."},
)
class CurrentSitesForm(RegistrarForm):
@ -618,6 +610,12 @@ class NoOtherContactsForm(RegistrarForm):
"we can contact to help us assess your eligibility for a .gov domain."
),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
)

View file

@ -213,6 +213,9 @@ class AuthorizingOfficialContactForm(ContactForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Overriding bc phone not required in this form
self.fields["phone"] = forms.IntegerField(required=False)
# Set custom error messages
self.fields["first_name"].error_messages = {
"required": "Enter the first name / given name of your authorizing official."
@ -227,7 +230,6 @@ class AuthorizingOfficialContactForm(ContactForm):
self.fields["email"].error_messages = {
"required": "Enter an email address in the required format, like name@example.com."
}
self.fields["phone"].error_messages["required"] = "Enter a phone number for your authorizing official."
class DomainSecurityEmailForm(forms.Form):

View file

@ -29,7 +29,7 @@ class Command(BaseCommand):
self.update_success = []
self.update_skipped = []
self.update_failed = []
self.expiration_minimum_cutoff = date(2023, 11, 15)
self.expiration_minimum_cutoff = date(2023, 11, 1)
self.expiration_maximum_cutoff = date(2023, 12, 30)
def add_arguments(self, parser):

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2023-12-13 15:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0056_alter_domain_state_alter_domainapplication_status_and_more"),
]
operations = [
migrations.AddField(
model_name="domainapplication",
name="submission_date",
field=models.DateField(blank=True, default=None, help_text="Date submitted", null=True),
),
]

View file

@ -64,7 +64,7 @@ class Contact(TimeStampedModel):
super().save(*args, **kwargs)
# Update the related User object's first_name and last_name
if self.user:
if self.user and (not self.user.first_name or not self.user.last_name):
self.user.first_name = self.first_name
self.user.last_name = self.last_name
self.user.save()

View file

@ -8,6 +8,7 @@ from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
from django.db import models
from django.utils import timezone
from typing import Any
@ -201,6 +202,14 @@ class Domain(TimeStampedModel, DomainHelper):
"""Get the `cr_date` element from the registry."""
return self._get_property("cr_date")
@creation_date.setter # type: ignore
def creation_date(self, cr_date: date):
"""
Direct setting of the creation date in the registry is not implemented.
Creation date can only be set by registry."""
raise NotImplementedError()
@Cache
def last_transferred_date(self) -> date:
"""Get the `tr_date` element from the registry."""
@ -976,6 +985,16 @@ class Domain(TimeStampedModel, DomainHelper):
def isActive(self):
return self.state == Domain.State.CREATED
def is_expired(self):
"""
Check if the domain's expiration date is in the past.
Returns True if expired, False otherwise.
"""
if self.expiration_date is None:
return True
now = timezone.now().date()
return self.expiration_date < now
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
"""Maps the Epp contact representation to a PublicContact object.
@ -1601,38 +1620,11 @@ class Domain(TimeStampedModel, DomainHelper):
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
"""Contact registry for info about a domain."""
try:
# get info from registry
data_response = self._get_or_create_domain()
cache = self._extract_data_from_response(data_response)
# remove null properties (to distinguish between "a value of None" and null)
cleaned = self._remove_null_properties(cache)
if "statuses" in cleaned:
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions)
# Capture and store old hosts and contacts from cache if they exist
old_cache_hosts = self._cache.get("hosts")
old_cache_contacts = self._cache.get("contacts")
if fetch_contacts:
cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", []))
if old_cache_hosts is not None:
logger.debug("resetting cleaned['hosts'] to old_cache_hosts")
cleaned["hosts"] = old_cache_hosts
if fetch_hosts:
cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", []))
if old_cache_contacts is not None:
cleaned["contacts"] = old_cache_contacts
# if expiration date from registry does not match what is in db,
# update the db
if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date:
self.expiration_date = cleaned["ex_date"]
self.save()
cleaned = self._clean_cache(cache, data_response)
self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts)
self._update_dates(cleaned)
self._cache = cleaned
@ -1640,6 +1632,7 @@ class Domain(TimeStampedModel, DomainHelper):
logger.error(e)
def _extract_data_from_response(self, data_response):
"""extract data from response from registry"""
data = data_response.res_data[0]
return {
"auth_info": getattr(data, "auth_info", ...),
@ -1654,6 +1647,15 @@ class Domain(TimeStampedModel, DomainHelper):
"up_date": getattr(data, "up_date", ...),
}
def _clean_cache(self, cache, data_response):
"""clean up the cache"""
# remove null properties (to distinguish between "a value of None" and null)
cleaned = self._remove_null_properties(cache)
if "statuses" in cleaned:
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions)
return cleaned
def _remove_null_properties(self, cache):
return {k: v for k, v in cache.items() if v is not ...}
@ -1667,6 +1669,42 @@ class Domain(TimeStampedModel, DomainHelper):
dnssec_data = extension
return dnssec_data
def _update_hosts_and_contacts(self, cleaned, fetch_hosts, fetch_contacts):
"""Capture and store old hosts and contacts from cache if they don't exist"""
old_cache_hosts = self._cache.get("hosts")
old_cache_contacts = self._cache.get("contacts")
if fetch_contacts:
cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", []))
if old_cache_hosts is not None:
logger.debug("resetting cleaned['hosts'] to old_cache_hosts")
cleaned["hosts"] = old_cache_hosts
if fetch_hosts:
cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", []))
if old_cache_contacts is not None:
cleaned["contacts"] = old_cache_contacts
def _update_dates(self, cleaned):
"""Update dates (expiration and creation) from cleaned"""
requires_save = False
# if expiration date from registry does not match what is in db,
# update the db
if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date:
self.expiration_date = cleaned["ex_date"]
requires_save = True
# if creation_date from registry does not match what is in db,
# update the db
if "cr_date" in cleaned and cleaned["cr_date"] != self.created_at:
self.created_at = cleaned["cr_date"]
requires_save = True
# if either registration date or creation date need updating
if requires_save:
self.save()
def _get_contacts(self, contacts):
choices = PublicContact.ContactTypeChoices
# We expect that all these fields get populated,

View file

@ -6,6 +6,7 @@ import logging
from django.apps import apps
from django.db import models
from django_fsm import FSMField, transition # type: ignore
from django.utils import timezone
from registrar.models.domain import Domain
from .utility.time_stamped_model import TimeStampedModel
@ -122,7 +123,7 @@ class DomainApplication(TimeStampedModel):
FEDERAL = (
"federal",
"Federal: an agency of the U.S. government's executive, legislative, or judicial branches",
"Federal: an agency of the U.S. governments legislative, executive, or judicial branches",
)
INTERSTATE = "interstate", "Interstate: an organization of two or more states"
STATE_OR_TERRITORY = (
@ -139,7 +140,7 @@ class DomainApplication(TimeStampedModel):
CITY = "city", "City: a city, town, township, village, etc."
SPECIAL_DISTRICT = (
"special_district",
"Special district: an independent organization within a single state",
"Special district: an independent government that delivers specialized, essential services",
)
SCHOOL_DISTRICT = (
"school_district",
@ -547,6 +548,14 @@ class DomainApplication(TimeStampedModel):
help_text="Acknowledged .gov acceptable use policy",
)
# submission date records when application is submitted
submission_date = models.DateField(
null=True,
blank=True,
default=None,
help_text="Date submitted",
)
def __str__(self):
try:
if self.requested_domain and self.requested_domain.name:
@ -585,7 +594,12 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
source=[ApplicationStatus.STARTED, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.WITHDRAWN],
source=[
ApplicationStatus.STARTED,
ApplicationStatus.IN_REVIEW,
ApplicationStatus.ACTION_NEEDED,
ApplicationStatus.WITHDRAWN,
],
target=ApplicationStatus.SUBMITTED,
)
def submit(self):
@ -607,13 +621,27 @@ class DomainApplication(TimeStampedModel):
if not DraftDomain.string_could_be_domain(self.requested_domain.name):
raise ValueError("Requested domain is not a valid domain name.")
# Update submission_date to today
self.submission_date = timezone.now().date()
self.save()
self._send_status_update_email(
"submission confirmation",
"emails/submission_confirmation.txt",
"emails/submission_confirmation_subject.txt",
)
@transition(field="status", source=ApplicationStatus.SUBMITTED, target=ApplicationStatus.IN_REVIEW)
@transition(
field="status",
source=[
ApplicationStatus.SUBMITTED,
ApplicationStatus.ACTION_NEEDED,
ApplicationStatus.APPROVED,
ApplicationStatus.REJECTED,
ApplicationStatus.INELIGIBLE,
],
target=ApplicationStatus.IN_REVIEW,
)
def in_review(self):
"""Investigate an application that has been submitted.
@ -627,7 +655,12 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED],
source=[
ApplicationStatus.IN_REVIEW,
ApplicationStatus.APPROVED,
ApplicationStatus.REJECTED,
ApplicationStatus.INELIGIBLE,
],
target=ApplicationStatus.ACTION_NEEDED,
)
def action_needed(self):
@ -646,8 +679,8 @@ class DomainApplication(TimeStampedModel):
source=[
ApplicationStatus.SUBMITTED,
ApplicationStatus.IN_REVIEW,
ApplicationStatus.ACTION_NEEDED,
ApplicationStatus.REJECTED,
ApplicationStatus.INELIGIBLE,
],
target=ApplicationStatus.APPROVED,
)
@ -684,7 +717,7 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW],
source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED],
target=ApplicationStatus.WITHDRAWN,
)
def withdraw(self):
@ -697,7 +730,7 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.APPROVED],
target=ApplicationStatus.REJECTED,
conditions=[domain_is_not_active],
)
@ -707,13 +740,17 @@ class DomainApplication(TimeStampedModel):
As side effects this will delete the domain and domain_information
(will cascade), and send an email notification."""
if self.status == self.ApplicationStatus.APPROVED:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
try:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error("Can't query an approved domain while attempting a DA reject()")
self._send_status_update_email(
"action needed",
@ -723,7 +760,12 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
source=[
ApplicationStatus.IN_REVIEW,
ApplicationStatus.ACTION_NEEDED,
ApplicationStatus.APPROVED,
ApplicationStatus.REJECTED,
],
target=ApplicationStatus.INELIGIBLE,
conditions=[domain_is_not_active],
)
@ -737,13 +779,17 @@ class DomainApplication(TimeStampedModel):
and domain_information (will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
try:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()")
self.creator.restrict_user()

View file

@ -229,6 +229,7 @@ class DomainInformation(TimeStampedModel):
da_dict.pop("alternative_domains", None)
da_dict.pop("requested_domain", None)
da_dict.pop("approved_domain", None)
da_dict.pop("submission_date", None)
other_contacts = da_dict.pop("other_contacts", [])
domain_info = cls(**da_dict)
domain_info.domain_application = domain_application

View file

@ -114,7 +114,7 @@ class UserGroup(Group):
)
cisa_analysts_group.save()
logger.debug("CISA Analyt permissions added to group " + cisa_analysts_group.name)
logger.debug("CISA Analyst permissions added to group " + cisa_analysts_group.name)
except Exception as e:
logger.error(f"Error creating analyst permissions group: {e}")

View file

@ -1,63 +0,0 @@
import logging
from typing import Dict
from django.forms import ModelChoiceField
logger = logging.getLogger(__name__)
class SortingDict:
"""Stores a sorting dictionary object"""
_sorting_dict: Dict[type, type] = {}
def __init__(self, model_list, sort_list):
self._sorting_dict = {
"dropDownSelected": self.convert_list_to_dict(model_list),
"sortBy": sort_list,
}
# Used in __init__ for model_list for performance reasons
def convert_list_to_dict(self, value_list):
"""Used internally to convert model_list to a dictionary"""
return {item: item for item in value_list}
def get_dict(self):
"""Grabs the associated dictionary item,
has two fields: 'dropDownSelected': model_list and 'sortBy': sort_list"""
# This should never happen so we need to log this
if self._sorting_dict is None:
raise ValueError("_sorting_dict was None")
return self._sorting_dict
class AdminFormOrderHelper:
"""A helper class to order a dropdown field in Django Admin,
takes the fields you want to order by as an array"""
# Used to keep track of how we want to order_by certain FKs
_sorting_list: list[SortingDict] = []
def __init__(self, sort: list[SortingDict]):
self._sorting_list = sort
def get_ordered_form_field(self, form_field, db_field) -> ModelChoiceField | None:
"""Orders the queryset for a ModelChoiceField
based on the order_by_dict dictionary"""
_order_by_list = []
for item in self._sorting_list:
item_dict = item.get_dict()
drop_down_selected = item_dict.get("dropDownSelected")
sort_by = item_dict.get("sortBy")
if db_field.name in drop_down_selected:
_order_by_list = sort_by
# Exit loop when order_by_list is found
break
# Only order if we choose to do so
# noqa for the linter... reduces readability otherwise
if _order_by_list is not None and _order_by_list != []: # noqa
form_field.queryset = form_field.queryset.order_by(*_order_by_list)
return form_field

View file

@ -1,27 +0,0 @@
from registrar.models.utility.admin_form_order_helper import (
AdminFormOrderHelper,
SortingDict,
)
class AdminSortFields:
# Used to keep track of how we want to order_by certain FKs
foreignkey_orderby_dict: list[SortingDict] = [
# foreign_key - order_by
# Handles fields that are sorted by 'first_name / last_name
SortingDict(
["submitter", "authorizing_official", "investigator", "creator", "user"],
["first_name", "last_name"],
),
# Handles fields that are sorted by 'name'
SortingDict(["domain", "requested_domain"], ["name"]),
SortingDict(["domain_application"], ["requested_domain__name"]),
]
# For readability purposes, but can be replaced with a one liner
def form_field_order_helper(self, form_field, db_field):
"""A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict)
.get_ordered_form_field(form_field, db_field)"""
form = AdminFormOrderHelper(self.foreignkey_orderby_dict)
return form.get_ordered_form_field(form_field, db_field)

View file

@ -23,7 +23,7 @@
{% endif %}
<p>
You must be an authorized user and need to be signed in to view this page.
Would you like to <a href="{% url 'login' %}"> try logging in again</a>?
<a href="{% url 'login' %}"> Try logging in again</a>.
</p>
<p>
If you'd like help with this error <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">contact us</a>.

View file

@ -6,7 +6,9 @@
Who is the authorizing official for your organization?
</h2>
<p>Your authorizing official is a person within your organization who can authorize your domain request. This person must be in a role of significant, executive responsibility within the organization.</p>
{% if not is_federal %}
<p>Your authorizing official is a person within your organization who can authorize your domain request. This person must be in a role of significant, executive responsibility within the organization.</p>
{% endif %}
<div class="ao_example">
{% include "includes/ao_example.html" %}
@ -25,17 +27,11 @@
{% input_with_errors forms.0.first_name %}
{% input_with_errors forms.0.middle_name %}
{% input_with_errors forms.0.last_name %}
{% input_with_errors forms.0.title %}
{% input_with_errors forms.0.email %}
{% with add_class="usa-input--medium" %}
{% input_with_errors forms.0.phone %}
{% endwith %}
</fieldset>
{% endblock %}

View file

@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load static %}
{% load url_helpers %}
{% block title %}Thanks for your domain request! | {% endblock %}
@ -14,21 +15,22 @@
/>
<h1>Thanks for your domain request!</h1>
</span>
<p>We'll email a copy of your request to you.</p>
<p>Well email a copy of your request to you.</p>
<h2>Next steps in this process</h2>
<p> We'll review your request. This usually takes 20 business days. During
this review we'll verify that your:</p>
<p> Well review your request. This usually takes 20 business days. During
this review well verify that:</p>
<ul class="usa-list">
<li>Organization is eligible for a .gov domain</li>
<li>Authorizing official approves your request</li>
<li>Domain meets our naming requirements</li>
<li>Your organization is eligible for a .gov domain.</li>
<li>You work at the organization and/or can make requests on its behalf.</li>
<li>Your requested domain meets our naming requirements.</li>
</ul>
<p> You can <a href="{% url 'home' %}">check the status</a>
of your request at any time. We'll email you with any questions or when we
complete our review.</p>
<p> Well email you if we have questions and when we complete our review. You can <a href="{% url 'home' %}">check the status</a>
of your request at any time on the registrar homepage.</p>
<p> <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us if you need help during this process</a>.</p>
</main>
{% endblock %}

View file

@ -105,7 +105,7 @@
aria-describedby="Are you sure you want to submit a domain request?"
data-force-action
>
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you wont be able to make further edits until its reviewed by our staff. Youll only be able to withdraw your request." modal_button=modal_button|safe %}
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you wont be able to edit it until we review it. Youll only be able to withdraw your request." modal_button=modal_button|safe %}
</div>
{% block after_form_content %}{% endblock %}

View file

@ -2,5 +2,7 @@
{% load static field_helpers %}
{% block form_fields %}
{% input_with_errors forms.0.no_other_contacts_rationale %}
{% with attr_maxlength=1000 %}
{% input_with_errors forms.0.no_other_contacts_rationale %}
{% endwith %}
{% endblock %}

View file

@ -19,18 +19,12 @@
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% input_with_errors form.phone %}
<button
type="submit"
class="usa-button"

View file

@ -6,7 +6,7 @@
<div class="margin-top-4 tablet:grid-col-10">
<div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%} dotgov-status-box--action-need{% endif %}"
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2{% if not domain.is_expired %}{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} dotgov-status-box--action-need{% endif %}{% endif %}"
role="region"
aria-labelledby="summary-box-key-information"
>
@ -17,7 +17,9 @@
<span class="text-bold text-primary-darker">
Status:
</span>
{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%}
{% if domain.is_expired %}
Expired
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%}
DNS needed
{% else %}
{{ domain.state|title }}
@ -26,13 +28,16 @@
</div>
</div>
<br>
{% include "includes/domain_dates.html" %}
{% url 'domain-dns-nameservers' pk=domain.id as url %}
{% if domain.nameservers|length > 0 %}
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=domain.is_editable %}
{% else %}
{% if domain.is_editable %}
<h2 class="margin-top-neg-1"> DNS name servers </h2>
<h2 class="margin-top-3"> DNS name servers </h2>
<p> No DNS name servers have been added yet. Before your domain can be used well need information about your domain name servers.</p>
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
{% else %}

View file

@ -12,7 +12,7 @@
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
{% url 'domain-dns-nameservers' pk=domain.id as url %}
<ul>
<ul class="usa-list">
<li><a href="{{ url }}">Name servers</a></li>
{% url 'domain-dns-dnssec' pk=domain.id as url %}

View file

@ -1,7 +1,7 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi.
{{ full_name }} has added you as a manager on {{ domain.name }}.
{{ requester_email }} has added you as a manager on {{ domain.name }}.
YOU NEED A LOGIN.GOV ACCOUNT
Youll need a Login.gov account to manage your .gov domain. Login.gov provides

View file

@ -39,7 +39,7 @@
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Domain name</th>
<th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Expires</th>
<th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
</tr>
@ -50,9 +50,11 @@
<th th scope="row" role="rowheader" data-label="Domain name">
{{ domain.name }}
</th>
<td data-sort-value="{{ domain.created_time|date:"U" }}" data-label="Date created">{{ domain.created_time|date }}</td>
<td data-sort-value="{{ domain.expiration_date|date:"U" }}" data-label="Expires">{{ domain.expiration_date|date }}</td>
<td data-label="Status">
{% if domain.state == "unknown" or domain.state == "dns needed"%}
{% if domain.is_expired %}
Expired
{% elif domain.state == "unknown" or domain.state == "dns needed"%}
DNS needed
{% else %}
{{ domain.state|title }}
@ -99,7 +101,7 @@
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Domain name</th>
<th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Date submitted</th>
<th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
</tr>
@ -110,7 +112,13 @@
<th th scope="row" role="rowheader" data-label="Domain name">
{{ application.requested_domain.name|default:"New domain request" }}
</th>
<td data-sort-value="{{ application.created_at|date:"U" }}" data-label="Date created">{{ application.created_at|date }}</td>
<td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
{% if application.submission_date %}
{{ application.submission_date|date }}
{% else %}
<span class="text-base">Not submitted</span>
{% endif %}
</td>
<td data-label="Status">{{ application.get_status_display }}</td>
<td>
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}

View file

@ -1,7 +1,9 @@
{% load url_helpers %}
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold" >
Next steps
Next steps in this process
</h2>
<p>We received your .gov domain request. Our next step is to review your request. This usually takes two weeks. Well email you with questions or when we complete our review. Contact us with any questions.</p>
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 20 business days. Well email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
Need to make changes?

View file

@ -0,0 +1,12 @@
{% if domain.expiration_date or domain.created_at %}
<p class="margin-y-0">
{% if domain.expiration_date %}
<strong class="text-primary-dark">Expires:</strong>
{{ domain.expiration_date|date }}
{% if domain.is_expired %} <span class="text-error"><strong>(expired)</strong></span>{% endif %}
<br/>
{% endif %}
{% if domain.created_at %}
<strong class="text-primary-dark">Date created:</strong> {{ domain.created_at|date }}{% endif %}
</p>
{% endif %}

View file

@ -462,7 +462,7 @@ def completed_application(
):
"""A completed domain application."""
if not user:
user = get_user_model().objects.create(username="username")
user = get_user_model().objects.create(username="username" + str(uuid.uuid4())[:8])
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",

View file

@ -15,13 +15,7 @@ from registrar.admin import (
ContactAdmin,
UserDomainRoleAdmin,
)
from registrar.models import (
Domain,
DomainApplication,
DomainInformation,
User,
DomainInvitation,
)
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
from registrar.models.user_domain_role import UserDomainRole
from .common import (
completed_application,
@ -322,6 +316,7 @@ class TestDomainApplicationAdmin(MockEppLib):
self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site)
self.superuser = create_superuser()
self.staffuser = create_user()
self.client = Client(HTTP_HOST="localhost:8080")
def test_short_org_name_in_applications_list(self):
"""
@ -623,6 +618,7 @@ class TestDomainApplicationAdmin(MockEppLib):
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
"submission_date",
"current_websites",
"other_contacts",
"alternative_domains",
@ -842,12 +838,224 @@ class TestDomainApplicationAdmin(MockEppLib):
with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db()
def test_has_correct_filters(self):
"""
This test verifies that DomainApplicationAdmin has the correct filters set up.
It retrieves the current list of filters from DomainApplicationAdmin
and checks that it matches the expected list of filters.
"""
request = self.factory.get("/")
request.user = self.superuser
# Grab the current list of table filters
readonly_fields = self.admin.get_list_filter(request)
expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter)
self.assertEqual(readonly_fields, expected_fields)
def test_table_sorted_alphabetically(self):
"""
This test verifies that the DomainApplicationAdmin table is sorted alphabetically
by the 'requested_domain__name' field.
It creates a list of DomainApplication instances in a non-alphabetical order,
then retrieves the queryset from the DomainApplicationAdmin and checks
that it matches the expected queryset,
which is sorted alphabetically by the 'requested_domain__name' field.
"""
# Creates a list of DomainApplications in scrambled order
multiple_unalphabetical_domain_objects("application")
request = self.factory.get("/")
request.user = self.superuser
# Get the expected list of alphabetically sorted DomainApplications
expected_order = DomainApplication.objects.order_by("requested_domain__name")
# Get the returned queryset
queryset = self.admin.get_queryset(request)
# Check the order
self.assertEqual(
list(queryset),
list(expected_order),
)
def test_displays_investigator_filter(self):
"""
This test verifies that the investigator filter in the admin interface for
the DomainApplication model displays correctly.
It creates two DomainApplication instances, each with a different investigator.
It then simulates a staff user logging in and applying the investigator filter
on the DomainApplication admin page.
We then test if the page displays the filter we expect, but we do not test
if we get back the correct response in the table. This is to isolate if
the filter displays correctly, when the filter isn't filtering correctly.
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainapplication/",
{
"investigator__id__exact": investigator_user.id,
},
follow=True,
)
# Then, test if the filter actually exists
self.assertIn("filters", response.context)
# Assert the content of filters and search_query
filters = response.context["filters"]
self.assertEqual(
filters,
[
{
"parameter_name": "investigator",
"parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator",
},
],
)
def test_investigator_filter_filters_correctly(self):
"""
This test verifies that the investigator filter in the admin interface for
the DomainApplication model works correctly.
It creates two DomainApplication instances, each with a different investigator.
It then simulates a staff user logging in and applying the investigator filter
on the DomainApplication admin page.
It then verifies that it was applied correctly.
The test checks that the response contains the expected DomainApplication pbjects
in the table.
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
# Create a second mock DomainApplication object, to test filtering
application: DomainApplication = generic_domain_object("application", "BadGuy")
another_user = User.objects.filter(username=application.investigator.username).get()
another_user.is_staff = True
another_user.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainapplication/",
{
"investigator__id__exact": investigator_user.id,
},
follow=True,
)
expected_name = "SomeGuy first_name:investigator SomeGuy last_name:investigator"
# We expect to see this four times, two of them are from the html for the filter,
# and the other two are the html from the list entry in the table.
self.assertContains(response, expected_name, count=4)
# Check that we don't also get the thing we aren't filtering for.
# We expect to see this two times in the filter
unexpected_name = "BadGuy first_name:investigator BadGuy last_name:investigator"
self.assertContains(response, unexpected_name, count=2)
def test_investigator_dropdown_displays_only_staff(self):
"""
This test verifies that the dropdown for the 'investigator' field in the DomainApplicationAdmin
interface only displays users who are marked as staff.
It creates two DomainApplication instances, one with an investigator
who is a staff user and another with an investigator who is not a staff user.
It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin
and checks that it matches the expected queryset, which only includes staff users.
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
# Create a mock DomainApplication object, with a user that is not staff
application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy")
investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get()
investigator_user_2.is_staff = False
investigator_user_2.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Get the actual field from the model's meta information
investigator_field = DomainApplication._meta.get_field("investigator")
# We should only be displaying staff users, in alphabetical order
expected_dropdown = list(User.objects.filter(is_staff=True))
current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset)
self.assertEqual(expected_dropdown, current_dropdown)
# Non staff users should not be in the list
self.assertNotIn(application_2, current_dropdown)
def test_investigator_list_is_alphabetically_sorted(self):
"""
This test verifies that filter list for the 'investigator'
is displayed alphabetically
"""
# Create a mock DomainApplication object, with a fake investigator
application: DomainApplication = generic_domain_object("application", "SomeGuy")
investigator_user = User.objects.filter(username=application.investigator.username).get()
investigator_user.is_staff = True
investigator_user.save()
application_2: DomainApplication = generic_domain_object("application", "AGuy")
investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get()
investigator_user_2.first_name = "AGuy"
investigator_user_2.is_staff = True
investigator_user_2.save()
application_3: DomainApplication = generic_domain_object("application", "FinalGuy")
investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get()
investigator_user_3.first_name = "FinalGuy"
investigator_user_3.is_staff = True
investigator_user_3.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
request = RequestFactory().get("/")
expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email"))
# Get the actual sorted list of investigators from the lookups method
actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)]
self.assertEqual(expected_list, actual_list)
def tearDown(self):
super().tearDown()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
User.objects.all().delete()
Contact.objects.all().delete()
Website.objects.all().delete()
class DomainInvitationAdminTest(TestCase):
@ -1110,8 +1318,8 @@ class AuditedAdminTest(TestCase):
tested_fields = [
DomainApplication.authorizing_official.field,
DomainApplication.submitter.field,
# DomainApplication.investigator.field,
# DomainApplication.creator.field,
DomainApplication.investigator.field,
DomainApplication.creator.field,
DomainApplication.requested_domain.field,
]

View file

@ -196,11 +196,6 @@ class TestFormValidation(MockEppLib):
["Response must be less than 1000 characters."],
)
def test_authorizing_official_phone_invalid(self):
"""Must be a valid phone number."""
form = AuthorizingOfficialForm(data={"phone": "boss@boss"})
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number "))
def test_your_contact_email_invalid(self):
"""must be a valid email address."""
form = YourContactForm(data={"email": "boss@boss"})

View file

@ -26,20 +26,52 @@ boto3_mocking.clients.register_handler("sesv2", MockSESClient)
# with AWS SES, so mock that out in all of these test cases
@boto3_mocking.patching
class TestDomainApplication(TestCase):
def setUp(self):
self.started_application = completed_application(
status=DomainApplication.ApplicationStatus.STARTED, name="started.gov"
)
self.submitted_application = completed_application(
status=DomainApplication.ApplicationStatus.SUBMITTED, name="submitted.gov"
)
self.in_review_application = completed_application(
status=DomainApplication.ApplicationStatus.IN_REVIEW, name="in-review.gov"
)
self.action_needed_application = completed_application(
status=DomainApplication.ApplicationStatus.ACTION_NEEDED, name="action-needed.gov"
)
self.approved_application = completed_application(
status=DomainApplication.ApplicationStatus.APPROVED, name="approved.gov"
)
self.withdrawn_application = completed_application(
status=DomainApplication.ApplicationStatus.WITHDRAWN, name="withdrawn.gov"
)
self.rejected_application = completed_application(
status=DomainApplication.ApplicationStatus.REJECTED, name="rejected.gov"
)
self.ineligible_application = completed_application(
status=DomainApplication.ApplicationStatus.INELIGIBLE, name="ineligible.gov"
)
def assertNotRaises(self, exception_type):
"""Helper method for testing allowed transitions."""
return self.assertRaises(Exception, None, exception_type)
def test_empty_create_fails(self):
"""Can't create a completely empty domain application."""
"""Can't create a completely empty domain application.
NOTE: something about theexception this test raises messes up with the
atomic block in a custom tearDown method for the parent test class."""
with self.assertRaisesRegex(IntegrityError, "creator"):
DomainApplication.objects.create()
def test_minimal_create(self):
"""Can create with just a creator."""
user, _ = User.objects.get_or_create()
user, _ = User.objects.get_or_create(username="testy")
application = DomainApplication.objects.create(creator=user)
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
def test_full_create(self):
"""Can create with all fields."""
user, _ = User.objects.get_or_create()
user, _ = User.objects.get_or_create(username="testy")
contact = Contact.objects.create()
com_website, _ = Website.objects.get_or_create(website="igorville.com")
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
@ -69,7 +101,7 @@ class TestDomainApplication(TestCase):
def test_domain_info(self):
"""Can create domain info with all fields."""
user, _ = User.objects.get_or_create()
user, _ = User.objects.get_or_create(username="testy")
contact = Contact.objects.create()
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
information = DomainInformation.objects.create(
@ -95,14 +127,14 @@ class TestDomainApplication(TestCase):
self.assertEqual(information.id, domain.domain_info.id)
def test_status_fsm_submit_fail(self):
user, _ = User.objects.get_or_create()
user, _ = User.objects.get_or_create(username="testy")
application = DomainApplication.objects.create(creator=user)
with self.assertRaises(ValueError):
# can't submit an application with a null domain name
application.submit()
def test_status_fsm_submit_succeed(self):
user, _ = User.objects.get_or_create()
user, _ = User.objects.get_or_create(username="testy")
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(creator=user, requested_domain=site)
# no submitter email so this emits a log warning
@ -112,7 +144,7 @@ class TestDomainApplication(TestCase):
def test_submit_sends_email(self):
"""Create an application and submit it and see if email was sent."""
user, _ = User.objects.get_or_create()
user, _ = User.objects.get_or_create(username="testy")
contact = Contact.objects.create(email="test@test.gov")
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
application = DomainApplication.objects.create(
@ -135,320 +167,251 @@ class TestDomainApplication(TestCase):
0,
)
def test_transition_not_allowed_submitted_submitted(self):
"""Create an application with status submitted and call submit
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_in_review_submitted(self):
"""Create an application with status in review and call submit
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_approved_submitted(self):
"""Create an application with status approved and call submit
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_rejected_submitted(self):
"""Create an application with status rejected and call submit
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_ineligible_submitted(self):
"""Create an application with status ineligible and call submit
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_started_in_review(self):
"""Create an application with status started and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_in_review_in_review(self):
"""Create an application with status in review and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_approved_in_review(self):
"""Create an application with status approved and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_action_needed_in_review(self):
"""Create an application with status action needed and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_rejected_in_review(self):
"""Create an application with status rejected and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_withdrawn_in_review(self):
"""Create an application with status withdrawn and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_ineligible_in_review(self):
"""Create an application with status ineligible and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_started_action_needed(self):
"""Create an application with status started and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_submitted_action_needed(self):
"""Create an application with status submitted and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_action_needed_action_needed(self):
"""Create an application with status action needed and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_approved_action_needed(self):
"""Create an application with status approved and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_withdrawn_action_needed(self):
"""Create an application with status withdrawn and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_ineligible_action_needed(self):
"""Create an application with status ineligible and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_started_approved(self):
"""Create an application with status started and call approve
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.approve()
def test_transition_not_allowed_approved_approved(self):
"""Create an application with status approved and call approve
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.approve()
def test_transition_not_allowed_action_needed_approved(self):
"""Create an application with status action needed and call approve
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.approve()
def test_transition_not_allowed_withdrawn_approved(self):
"""Create an application with status withdrawn and call approve
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.approve()
def test_transition_not_allowed_started_withdrawn(self):
"""Create an application with status started and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_approved_withdrawn(self):
"""Create an application with status approved and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_action_needed_withdrawn(self):
"""Create an application with status action needed and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_rejected_withdrawn(self):
"""Create an application with status rejected and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_withdrawn_withdrawn(self):
"""Create an application with status withdrawn and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_ineligible_withdrawn(self):
"""Create an application with status ineligible and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_started_rejected(self):
"""Create an application with status started and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_submitted_rejected(self):
"""Create an application with status submitted and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_action_needed_rejected(self):
"""Create an application with status action needed and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_withdrawn_rejected(self):
"""Create an application with status withdrawn and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_rejected_rejected(self):
"""Create an application with status rejected and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_ineligible_rejected(self):
"""Create an application with status ineligible and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_submit_transition_allowed(self):
"""
Test that calling submit from allowable statuses does raises TransitionNotAllowed.
"""
test_cases = [
(self.started_application, TransitionNotAllowed),
(self.in_review_application, TransitionNotAllowed),
(self.action_needed_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.submit()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_submit_transition_not_allowed(self):
"""
Test that calling submit against transition rules raises TransitionNotAllowed.
"""
test_cases = [
(self.submitted_application, TransitionNotAllowed),
(self.approved_application, TransitionNotAllowed),
(self.rejected_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.submit()
def test_in_review_transition_allowed(self):
"""
Test that calling in_review from allowable statuses does raises TransitionNotAllowed.
"""
test_cases = [
(self.submitted_application, TransitionNotAllowed),
(self.action_needed_application, TransitionNotAllowed),
(self.approved_application, TransitionNotAllowed),
(self.rejected_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.in_review()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_in_review_transition_not_allowed(self):
"""
Test that calling in_review against transition rules raises TransitionNotAllowed.
"""
test_cases = [
(self.started_application, TransitionNotAllowed),
(self.in_review_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.in_review()
def test_action_needed_transition_allowed(self):
"""
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
"""
test_cases = [
(self.in_review_application, TransitionNotAllowed),
(self.approved_application, TransitionNotAllowed),
(self.rejected_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.action_needed()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_action_needed_transition_not_allowed(self):
"""
Test that calling action_needed against transition rules raises TransitionNotAllowed.
"""
test_cases = [
(self.started_application, TransitionNotAllowed),
(self.submitted_application, TransitionNotAllowed),
(self.action_needed_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.action_needed()
def test_approved_transition_allowed(self):
"""
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
"""
test_cases = [
(self.submitted_application, TransitionNotAllowed),
(self.in_review_application, TransitionNotAllowed),
(self.action_needed_application, TransitionNotAllowed),
(self.rejected_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.approve()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_approved_transition_not_allowed(self):
"""
Test that calling action_needed against transition rules raises TransitionNotAllowed.
"""
test_cases = [
(self.started_application, TransitionNotAllowed),
(self.approved_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.approve()
def test_withdraw_transition_allowed(self):
"""
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
"""
test_cases = [
(self.submitted_application, TransitionNotAllowed),
(self.in_review_application, TransitionNotAllowed),
(self.action_needed_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.withdraw()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_withdraw_transition_not_allowed(self):
"""
Test that calling action_needed against transition rules raises TransitionNotAllowed.
"""
test_cases = [
(self.started_application, TransitionNotAllowed),
(self.approved_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed),
(self.rejected_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.withdraw()
def test_reject_transition_allowed(self):
"""
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
"""
test_cases = [
(self.in_review_application, TransitionNotAllowed),
(self.action_needed_application, TransitionNotAllowed),
(self.approved_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.reject()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_reject_transition_not_allowed(self):
"""
Test that calling action_needed against transition rules raises TransitionNotAllowed.
"""
test_cases = [
(self.started_application, TransitionNotAllowed),
(self.submitted_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed),
(self.rejected_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.reject()
def test_reject_with_prejudice_transition_allowed(self):
"""
Test that calling action_needed from allowable statuses does raises TransitionNotAllowed.
"""
test_cases = [
(self.in_review_application, TransitionNotAllowed),
(self.action_needed_application, TransitionNotAllowed),
(self.approved_application, TransitionNotAllowed),
(self.rejected_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
try:
application.reject_with_prejudice()
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_reject_with_prejudice_transition_not_allowed(self):
"""
Test that calling action_needed against transition rules raises TransitionNotAllowed.
"""
test_cases = [
(self.started_application, TransitionNotAllowed),
(self.submitted_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed),
]
for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type):
application.reject_with_prejudice()
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call reject against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
self.approved_application.approved_domain = domain
self.approved_application.save()
# Define a custom implementation for is_active
def custom_is_active(self):
@ -458,70 +421,15 @@ class TestDomainApplication(TestCase):
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_started_ineligible(self):
"""Create an application with status started and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_submitted_ineligible(self):
"""Create an application with status submitted and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_action_needed_ineligible(self):
"""Create an application with status action needed and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_withdrawn_ineligible(self):
"""Create an application with status withdrawn and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_rejected_ineligible(self):
"""Create an application with status rejected and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_ineligible_ineligible(self):
"""Create an application with status ineligible and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
self.approved_application.reject()
def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call reject_with_prejudice against transition rules"""
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
self.approved_application.approved_domain = domain
self.approved_application.save()
# Define a custom implementation for is_active
def custom_is_active(self):
@ -531,7 +439,7 @@ class TestDomainApplication(TestCase):
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
self.approved_application.reject_with_prejudice()
class TestPermissions(TestCase):
@ -672,6 +580,12 @@ class TestUser(TestCase):
class TestContact(TestCase):
def setUp(self):
self.email_for_invalid = "intern@igorville.gov"
self.invalid_user, _ = User.objects.get_or_create(
username=self.email_for_invalid, email=self.email_for_invalid, first_name="", last_name=""
)
self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user)
self.email = "mayor@igorville.gov"
self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski")
self.contact, _ = Contact.objects.get_or_create(user=self.user)
@ -683,6 +597,31 @@ class TestContact(TestCase):
def test_saving_contact_updates_user_first_last_names(self):
"""When a contact is updated, we propagate the changes to the linked user if it exists."""
# User and Contact are created and linked as expected.
# An empty User object should create an empty contact.
self.assertEqual(self.invalid_contact.first_name, "")
self.assertEqual(self.invalid_contact.last_name, "")
self.assertEqual(self.invalid_user.first_name, "")
self.assertEqual(self.invalid_user.last_name, "")
# Manually update the contact - mimicking production (pre-existing data)
self.invalid_contact.first_name = "Joey"
self.invalid_contact.last_name = "Baloney"
self.invalid_contact.save()
# Refresh the user object to reflect the changes made in the database
self.invalid_user.refresh_from_db()
# Updating the contact's first and last names propagate to the user
self.assertEqual(self.invalid_contact.first_name, "Joey")
self.assertEqual(self.invalid_contact.last_name, "Baloney")
self.assertEqual(self.invalid_user.first_name, "Joey")
self.assertEqual(self.invalid_user.last_name, "Baloney")
def test_saving_contact_does_not_update_user_first_last_names(self):
"""When a contact is updated, we avoid propagating the changes to the linked user if it already has a value"""
# User and Contact are created and linked as expected
self.assertEqual(self.contact.first_name, "Jeff")
self.assertEqual(self.contact.last_name, "Lebowski")
@ -699,11 +638,11 @@ class TestContact(TestCase):
# Updating the contact's first and last names propagate to the user
self.assertEqual(self.contact.first_name, "Joey")
self.assertEqual(self.contact.last_name, "Baloney")
self.assertEqual(self.user.first_name, "Joey")
self.assertEqual(self.user.last_name, "Baloney")
self.assertEqual(self.user.first_name, "Jeff")
self.assertEqual(self.user.last_name, "Lebowski")
def test_saving_contact_does_not_update_user_email(self):
"""When a contact's email is updated, the change is not propagated to the lined user."""
"""When a contact's email is updated, the change is not propagated to the user."""
self.contact.email = "joey.baloney@diaperville.com"
self.contact.save()
@ -713,3 +652,16 @@ class TestContact(TestCase):
# Updating the contact's email does not propagate
self.assertEqual(self.contact.email, "joey.baloney@diaperville.com")
self.assertEqual(self.user.email, "mayor@igorville.gov")
def test_saving_contact_does_not_update_user_email_when_none(self):
"""When a contact's email is updated, and the first/last name is none,
the change is not propagated to the user."""
self.invalid_contact.email = "joey.baloney@diaperville.com"
self.invalid_contact.save()
# Refresh the user object to reflect the changes made in the database
self.invalid_user.refresh_from_db()
# Updating the contact's email does not propagate
self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com")
self.assertEqual(self.invalid_user.email, "intern@igorville.gov")

View file

@ -789,7 +789,6 @@ class TestRegistrantContacts(MockEppLib):
self.assertEqual(expected_contact.email, actual_contact.email)
def test_convert_public_contact_to_epp(self):
self.maxDiff = None
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
dummy_contact = domain.get_default_security_contact()
test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__
@ -1995,6 +1994,9 @@ class TestExpirationDate(MockEppLib):
"""
super().setUp()
# for the tests, need a domain in the ready state
# mock data for self.domain includes the following dates:
# cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35)
# ex_date=datetime.date(2023, 5, 25)
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# for the test, need a domain that will raise an exception
self.domain_w_error, _ = Domain.objects.get_or_create(name="fake-error.gov", state=Domain.State.READY)
@ -2020,6 +2022,23 @@ class TestExpirationDate(MockEppLib):
with self.assertRaises(RegistryError):
self.domain_w_error.renew_domain()
def test_is_expired(self):
"""assert that is_expired returns true for expiration_date in past"""
# force fetch_cache to be called
self.domain.statuses
self.assertTrue(self.domain.is_expired)
def test_is_not_expired(self):
"""assert that is_expired returns false for expiration in future"""
# to do this, need to mock value returned from timezone.now
# set now to 2023-01-01
mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0)
# force fetch_cache which sets the expiration date to 2023-05-25
self.domain.statuses
with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
self.assertFalse(self.domain.is_expired())
def test_expiration_date_updated_on_info_domain_call(self):
"""assert that expiration date in db is updated on info domain call"""
# force fetch_cache to be called
@ -2028,6 +2047,36 @@ class TestExpirationDate(MockEppLib):
self.assertEquals(self.domain.expiration_date, test_date)
class TestCreationDate(MockEppLib):
"""Created_at in domain model is updated from EPP"""
def setUp(self):
"""
Domain exists in registry
"""
super().setUp()
# for the tests, need a domain with a creation date
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# creation_date returned from mockDataInfoDomain with creation date:
# cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35)
self.creation_date = datetime.datetime(2023, 5, 25, 19, 45, 35)
def tearDown(self):
Domain.objects.all().delete()
super().tearDown()
def test_creation_date_setter_not_implemented(self):
"""assert that the setter for creation date is not implemented and will raise error"""
with self.assertRaises(NotImplementedError):
self.domain.creation_date = datetime.date.today()
def test_creation_date_updated_on_info_domain_call(self):
"""assert that creation date in db is updated on info domain call"""
# force fetch_cache to be called
self.domain.statuses
self.assertEquals(self.domain.created_at, self.creation_date)
class TestAnalystClientHold(MockEppLib):
"""Rule: Analysts may suspend or restore a domain by using client hold"""

View file

@ -158,7 +158,6 @@ class CsvReportsTest(TestCase):
@boto3_mocking.patching
def test_load_federal_report(self):
"""Tests the get_current_federal api endpoint"""
self.maxDiff = None
mock_client = MagicMock()
mock_client_instance = mock_client.return_value

View file

@ -6,7 +6,6 @@ from django.test import Client, TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from .common import MockEppLib, completed_application, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -100,7 +99,7 @@ class LoggedInTests(TestWithUser):
response = self.client.get("/")
# count = 2 because it is also in screenreader content
self.assertContains(response, "igorville.gov", count=2)
self.assertContains(response, "DNS needed")
self.assertContains(response, "Expired")
# clean up
role.delete()
@ -257,7 +256,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
ao_form["authorizing_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester"
ao_form["authorizing_official-email"] = "testy@town.com"
ao_form["authorizing_official-phone"] = "(201) 555 5555"
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -268,7 +266,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertEqual(application.authorizing_official.last_name, "Tester ATO")
self.assertEqual(application.authorizing_official.title, "Chief Tester")
self.assertEqual(application.authorizing_official.email, "testy@town.com")
self.assertEqual(application.authorizing_official.phone, "(201) 555 5555")
# the post request should return a redirect to the next form in
# the application
self.assertEqual(ao_result.status_code, 302)
@ -458,7 +455,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertContains(review_page, "Tester ATO")
self.assertContains(review_page, "Chief Tester")
self.assertContains(review_page, "testy@town.com")
self.assertContains(review_page, "(201) 555-5555")
self.assertContains(review_page, "city.com")
self.assertContains(review_page, "city.gov")
self.assertContains(review_page, "city1.gov")
@ -886,7 +882,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
ao_form["authorizing_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester"
ao_form["authorizing_official-email"] = "testy@town.com"
ao_form["authorizing_official-phone"] = "(201) 555 5555"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_result = ao_form.submit()
@ -1335,6 +1330,12 @@ class TestDomainDetail(TestDomainOverview):
class TestDomainManagers(TestDomainOverview):
def tearDown(self):
"""Ensure that the user has its original permissions"""
super().tearDown()
self.user.is_staff = False
self.user.save()
def test_domain_managers(self):
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
self.assertContains(response, "Domain managers")
@ -1438,6 +1439,7 @@ class TestDomainManagers(TestDomainOverview):
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
@ -1445,6 +1447,189 @@ class TestDomainManagers(TestDomainOverview):
Content=ANY,
)
@boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester_non_existent(self):
"""Inviting a non existent user sends them an email, with email as the name."""
# make sure there is no user with this email
email_address = "mayor@igorville.gov"
User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [email_address]},
Content=ANY,
)
# Check the arguments passed to send_email method
_, kwargs = mock_client_instance.send_email.call_args
# Extract the email content, and check that the message is as we expect
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("info@example.com", email_content)
# Check that the requesters first/last name do not exist
self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester(self):
"""Inviting a user sends them an email, with email as the name."""
# Create a fake user object
email_address = "mayor@igorville.gov"
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [email_address]},
Content=ANY,
)
# Check the arguments passed to send_email method
_, kwargs = mock_client_instance.send_email.call_args
# Extract the email content, and check that the message is as we expect
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("info@example.com", email_content)
# Check that the requesters first/last name do not exist
self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
def test_domain_invitation_email_has_email_as_requester_staff(self):
"""Inviting a user sends them an email, with email as the name."""
# Create a fake user object
email_address = "mayor@igorville.gov"
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
# Make sure the user is staff
self.user.is_staff = True
self.user.save()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [email_address]},
Content=ANY,
)
# Check the arguments passed to send_email method
_, kwargs = mock_client_instance.send_email.call_args
# Extract the email content, and check that the message is as we expect
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("help@get.gov", email_content)
# Check that the requesters first/last name do not exist
self.assertNotIn("First", email_content)
self.assertNotIn("Last", email_content)
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
def test_domain_invitation_email_displays_error_non_existent(self):
"""Inviting a non existent user sends them an email, with email as the name."""
# make sure there is no user with this email
email_address = "mayor@igorville.gov"
User.objects.filter(email=email_address).delete()
# Give the user who is sending the email an invalid email address
self.user.email = ""
self.user.save()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_error_message = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with patch("django.contrib.messages.error") as mock_error_message:
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit().follow()
expected_message_content = "Can't send invitation email. No email is associated with your account."
# Grab the message content
returned_error_message = mock_error_message.call_args[0][1]
# Check that the message content is what we expect
self.assertEqual(expected_message_content, returned_error_message)
@boto3_mocking.patching
def test_domain_invitation_email_displays_error(self):
"""When the requesting user has no email, an error is displayed"""
# make sure there is no user with this email
# Create a fake user object
email_address = "mayor@igorville.gov"
User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
# Give the user who is sending the email an invalid email address
self.user.email = ""
self.user.save()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
mock_client = MagicMock()
mock_error_message = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with patch("django.contrib.messages.error") as mock_error_message:
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit().follow()
expected_message_content = "Can't send invitation email. No email is associated with your account."
# Grab the message content
returned_error_message = mock_error_message.call_args[0][1]
# Check that the message content is what we expect
self.assertEqual(expected_message_content, returned_error_message)
def test_domain_invitation_cancel(self):
"""Posting to the delete view deletes an invitation."""
email_address = "mayor@igorville.gov"
@ -2229,6 +2414,33 @@ class TestApplicationStatus(TestWithUser, WebTest):
home_page = self.app.get("/")
self.assertContains(home_page, "Withdrawn")
def test_application_withdraw_no_permissions(self):
"""Can't withdraw applications as a restricted user."""
self.user.status = User.RESTRICTED
self.user.save()
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
application.save()
home_page = self.app.get("/")
self.assertContains(home_page, "city.gov")
# click the "Manage" link
detail_page = home_page.click("Manage", index=0)
self.assertContains(detail_page, "city.gov")
self.assertContains(detail_page, "city1.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
# Restricted user trying to withdraw results in 403 error
with less_console_noise():
for url_name in [
"application-withdraw-confirmation",
"application-withdrawn",
]:
with self.subTest(url_name=url_name):
page = self.client.get(reverse(url_name, kwargs={"pk": application.pk}))
self.assertEqual(page.status_code, 403)
def test_application_status_no_permissions(self):
"""Can't access applications without being the creator."""
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)

View file

@ -13,7 +13,11 @@ from registrar.models import DomainApplication
from registrar.utility import StrEnum
from registrar.views.utility import StepsHelper
from .utility import DomainApplicationPermissionView, ApplicationWizardPermissionView
from .utility import (
DomainApplicationPermissionView,
DomainApplicationPermissionWithdrawView,
ApplicationWizardPermissionView,
)
logger = logging.getLogger(__name__)
@ -544,7 +548,7 @@ class ApplicationStatus(DomainApplicationPermissionView):
template_name = "application_status.html"
class ApplicationWithdrawConfirmation(DomainApplicationPermissionView):
class ApplicationWithdrawConfirmation(DomainApplicationPermissionWithdrawView):
"""This page will ask user to confirm if they want to withdraw
The DomainApplicationPermissionView restricts access so that only the
@ -554,7 +558,7 @@ class ApplicationWithdrawConfirmation(DomainApplicationPermissionView):
template_name = "application_withdraw_confirmation.html"
class ApplicationWithdrawn(DomainApplicationPermissionView):
class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
# this view renders no template
template_name = ""

View file

@ -17,7 +17,6 @@ from django.views.generic.edit import FormMixin
from registrar.models import (
Domain,
DomainInformation,
DomainInvitation,
User,
UserDomainRole,
@ -644,21 +643,27 @@ class DomainAddUserView(DomainFormBaseView):
"""Get an absolute URL for this domain."""
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
def _send_domain_invitation_email(self, email: str, add_success=True):
def _send_domain_invitation_email(self, email: str, requester: User, add_success=True):
"""Performs the sending of the domain invitation email,
does not make a domain information object
email: string- email to send to
add_success: bool- default True indicates:
adding a success message to the view if the email sending succeeds"""
# created a new invitation in the database, so send an email
domainInfoResults = DomainInformation.objects.filter(domain=self.object)
domainInfo = domainInfoResults.first()
first = ""
last = ""
if domainInfo is not None:
first = domainInfo.creator.first_name
last = domainInfo.creator.last_name
full_name = f"{first} {last}"
# Set a default email address to send to for staff
requester_email = "help@get.gov"
# Check if the email requester has a valid email address
if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
requester_email = requester.email
elif not requester.is_staff:
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
logger.error(
f"Can't send email to '{email}' on domain '{self.object}'."
f"No email exists for the requester '{requester.username}'.",
exc_info=True,
)
return None
try:
send_templated_email(
@ -668,7 +673,7 @@ class DomainAddUserView(DomainFormBaseView):
context={
"domain_url": self._domain_abs_url(),
"domain": self.object,
"full_name": full_name,
"requester_email": requester_email,
},
)
except EmailSendingError:
@ -683,7 +688,7 @@ class DomainAddUserView(DomainFormBaseView):
if add_success:
messages.success(self.request, f"Invited {email} to this domain.")
def _make_invitation(self, email_address: str):
def _make_invitation(self, email_address: str, requester: User):
"""Make a Domain invitation for this email and redirect with a message."""
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
if not created:
@ -693,21 +698,22 @@ class DomainAddUserView(DomainFormBaseView):
f"{email_address} has already been invited to this domain.",
)
else:
self._send_domain_invitation_email(email=email_address)
self._send_domain_invitation_email(email=email_address, requester=requester)
return redirect(self.get_success_url())
def form_valid(self, form):
"""Add the specified user on this domain."""
requested_email = form.cleaned_data["email"]
requester = self.request.user
# look up a user with that email
try:
requested_user = User.objects.get(email=requested_email)
except User.DoesNotExist:
# no matching user, go make an invitation
return self._make_invitation(requested_email)
return self._make_invitation(requested_email, requester)
else:
# if user already exists then just send an email
self._send_domain_invitation_email(requested_email, add_success=False)
self._send_domain_invitation_email(requested_email, requester, add_success=False)
try:
UserDomainRole.objects.create(

View file

@ -1,7 +1,6 @@
from django.db.models import F
from django.shortcuts import render
from registrar.models import DomainApplication
from registrar.models import DomainApplication, Domain, UserDomainRole
def index(request):
@ -14,12 +13,9 @@ def index(request):
# the active applications table
context["domain_applications"] = applications.exclude(status="approved")
domains = request.user.permissions.values(
"role",
pk=F("domain__id"),
name=F("domain__name"),
created_time=F("domain__created_at"),
state=F("domain__state"),
)
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
domains = Domain.objects.filter(id__in=domain_ids)
context["domains"] = domains
return render(request, "home.html", context)

View file

@ -4,6 +4,7 @@ from .always_404 import always_404
from .permission_views import (
DomainPermissionView,
DomainApplicationPermissionView,
DomainApplicationPermissionWithdrawView,
DomainInvitationPermissionDeleteView,
ApplicationWizardPermissionView,
)

View file

@ -26,7 +26,8 @@ class PermissionsLoginMixin(PermissionRequiredMixin):
class DomainPermission(PermissionsLoginMixin):
"""Does the logged-in user have access to this domain?"""
"""Permission mixin that redirects to domain if user has access,
otherwise 403"""
def has_permission(self):
"""Check if this user has access to this domain.
@ -134,7 +135,8 @@ class DomainPermission(PermissionsLoginMixin):
class DomainApplicationPermission(PermissionsLoginMixin):
"""Does the logged-in user have access to this domain application?"""
"""Permission mixin that redirects to domain application if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to this domain application.
@ -154,9 +156,33 @@ class DomainApplicationPermission(PermissionsLoginMixin):
return True
class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
"""Permission mixin that redirects to withdraw action on domain application
if user has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to withdraw this domain application."""
if not self.request.user.is_authenticated:
return False
# user needs to be the creator of the application
# this query is empty if there isn't a domain application with this
# id and this user as creator
if not DomainApplication.objects.filter(creator=self.request.user, id=self.kwargs["pk"]).exists():
return False
# Restricted users should not be able to withdraw domain requests
if self.request.user.is_restricted():
return False
return True
class ApplicationWizardPermission(PermissionsLoginMixin):
"""Does the logged-in user have permission to start or edit an application?"""
"""Permission mixin that redirects to start or edit domain application if
user has access, otherwise 403"""
def has_permission(self):
"""Check if this user has permission to start or edit an application.
@ -173,7 +199,8 @@ class ApplicationWizardPermission(PermissionsLoginMixin):
class DomainInvitationPermission(PermissionsLoginMixin):
"""Does the logged-in user have access to this domain invitation?
"""Permission mixin that redirects to domain invitation if user has
access, otherwise 403"
A user has access to a domain invitation if they have a role on the
associated domain.

View file

@ -8,6 +8,7 @@ from registrar.models import Domain, DomainApplication, DomainInvitation
from .mixins import (
DomainPermission,
DomainApplicationPermission,
DomainApplicationPermissionWithdraw,
DomainInvitationPermission,
ApplicationWizardPermission,
)
@ -74,6 +75,26 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
raise NotImplementedError
class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC):
"""Abstract base view for domain application withdraw function
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
# DetailView property for what model this is viewing
model = DomainApplication
# variable name in template context for the model object
context_object_name = "domainapplication"
# Abstract property enforces NotImplementedError on an attribute.
@property
@abc.abstractmethod
def template_name(self):
raise NotImplementedError
class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC):
"""Abstract base view for the application form that enforces permissions

View file

@ -1,29 +1,29 @@
-i https://pypi.python.org/simple
annotated-types==0.6.0; python_version >= '3.8'
asgiref==3.7.2; python_version >= '3.7'
boto3==1.28.79; python_version >= '3.7'
botocore==1.31.79; python_version >= '3.7'
boto3==1.33.7; python_version >= '3.7'
botocore==1.33.7; python_version >= '3.7'
cachetools==5.3.2; python_version >= '3.7'
certifi==2023.7.22; python_version >= '3.6'
certifi==2023.11.17; python_version >= '3.6'
cfenv==0.5.3
cffi==1.16.0; python_version >= '3.8'
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
cryptography==41.0.6; python_version >= '3.7'
cryptography==41.0.7; python_version >= '3.7'
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
dj-database-url==2.1.0
dj-email-url==1.0.6
django==4.2.7; python_version >= '3.8'
django-allow-cidr==0.7.1
django-auditlog==2.3.0; python_version >= '3.7'
django-cache-url==3.4.4
django-cors-headers==4.3.0; python_version >= '3.8'
django-cache-url==3.4.5
django-cors-headers==4.3.1; python_version >= '3.8'
django-csp==3.7
django-fsm==2.8.1
django-login-required-middleware==0.9.0
django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8'
django-widget-tweaks==1.5.0; python_version >= '3.8'
environs[django]==9.5.0; python_version >= '3.6'
faker==19.13.0; python_version >= '3.8'
faker==20.1.0; python_version >= '3.8'
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
furl==2.1.3
future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
@ -31,28 +31,28 @@ gevent==23.9.1; python_version >= '3.8'
geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4
greenlet==3.0.1; python_version >= '3.7'
gunicorn==21.2.0; python_version >= '3.5'
idna==3.4; python_version >= '3.5'
idna==3.6; python_version >= '3.5'
jmespath==1.0.1; python_version >= '3.7'
lxml==4.9.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
mako==1.2.4; python_version >= '3.7'
mako==1.3.0; python_version >= '3.8'
markupsafe==2.1.3; python_version >= '3.7'
marshmallow==3.20.1; python_version >= '3.8'
oic==1.6.1; python_version ~= '3.7'
orderedmultidict==1.0.1
packaging==23.2; python_version >= '3.7'
phonenumberslite==8.13.24
phonenumberslite==8.13.26
psycopg2-binary==2.9.9; python_version >= '3.7'
pycparser==2.21
pycryptodomex==3.19.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pydantic==2.4.2; python_version >= '3.7'
pydantic-core==2.10.1; python_version >= '3.7'
pydantic-settings==2.0.3; python_version >= '3.7'
pydantic==2.5.2; python_version >= '3.7'
pydantic-core==2.14.5; python_version >= '3.7'
pydantic-settings==2.1.0; python_version >= '3.8'
pyjwkest==1.4.2
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dotenv==1.0.0; python_version >= '3.8'
requests==2.31.0; python_version >= '3.7'
s3transfer==0.7.0; python_version >= '3.7'
setuptools==68.2.2; python_version >= '3.8'
s3transfer==0.8.2; python_version >= '3.7'
setuptools==69.0.2; python_version >= '3.8'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.4; python_version >= '3.5'
typing-extensions==4.8.0; python_version >= '3.8'