Merge branch 'main' of https://github.com/cisagov/manage.get.gov into es/1975-fed-agency-update-script

This commit is contained in:
Rebecca Hsieh 2024-04-26 12:58:17 -07:00
commit 4021b8a66c
No known key found for this signature in database
40 changed files with 3141 additions and 725 deletions

View file

@ -13,9 +13,6 @@
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.
4. 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.
5. Run `docker-compose build` to build a new image for local development with the updated dependencies.
4. Run `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.

View file

@ -9,7 +9,7 @@ cfenv = "*"
django-cors-headers = "*"
pycryptodomex = "*"
django-allow-cidr = "*"
django-auditlog = "*"
django-auditlog = "2.3.0"
django-csp = "*"
environs = {extras=["django"]}
Faker = "*"
@ -21,7 +21,7 @@ whitenoise = "*"
django-widget-tweaks = "*"
cachetools = "*"
requests = "*"
django-fsm = "*"
django-fsm = "2.8.1"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"}
boto3 = "*"
typing-extensions ='*'

658
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "52143c73ccc59cd3dd6a1294a9352dbae009ebfc6e3ca5d018b8484275e2b6f8"
"sha256": "ce10883aef7e1ce10421d99b3ac35ebf419857a3fe468f0e2d93785f4323eaa8"
},
"pipfile-spec": 6,
"requires": {},
@ -32,20 +32,20 @@
},
"boto3": {
"hashes": [
"sha256:7ce8c9a50af2f8a159a0dd86b40011d8dfdaba35005a118e51cd3ac72dc630f1",
"sha256:d786e7fbe3c4152866199786468a625dc77b9f27294cd7ad4f63cd2e0c927287"
"sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87",
"sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.34.71"
"version": "==1.34.88"
},
"botocore": {
"hashes": [
"sha256:3bc9e23aee73fe6f097823d61f79a8877790436038101a83fa96c7593e8109f8",
"sha256:c58f9ed71af2ea53d24146187130541222d7de8c27eb87d23f15457e7b83d88b"
"sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e",
"sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b"
],
"markers": "python_version >= '3.8'",
"version": "==1.34.71"
"version": "==1.34.88"
},
"cachetools": {
"hashes": [
@ -392,12 +392,12 @@
},
"faker": {
"hashes": [
"sha256:998c29ee7d64429bd59204abffa9ba11f784fb26c7b9df4def78d1a70feb36a7",
"sha256:a5ddccbe97ab691fad6bd8036c31f5697cfaa550e62e000078d1935fa8a7ec2e"
"sha256:34b947581c2bced340c39b35f89dbfac4f356932cfff8fe893bde854903f0e6e",
"sha256:adb98e771073a06bdc5d2d6710d8af07ac5da64c8dc2ae3b17bb32319e66fd82"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==24.4.0"
"version": "==24.11.0"
},
"fred-epplib": {
"git": "https://github.com/cisagov/epplib.git",
@ -533,20 +533,20 @@
},
"gunicorn": {
"hashes": [
"sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0",
"sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"
"sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9",
"sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"
],
"index": "pypi",
"markers": "python_version >= '3.5'",
"version": "==21.2.0"
"markers": "python_version >= '3.7'",
"version": "==22.0.0"
},
"idna": {
"hashes": [
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
],
"markers": "python_version >= '3.5'",
"version": "==3.6"
"version": "==3.7"
},
"jmespath": {
"hashes": [
@ -558,95 +558,172 @@
},
"lxml": {
"hashes": [
"sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01",
"sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f",
"sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1",
"sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431",
"sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8",
"sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623",
"sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a",
"sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1",
"sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6",
"sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67",
"sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890",
"sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372",
"sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c",
"sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb",
"sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df",
"sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84",
"sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6",
"sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45",
"sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936",
"sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca",
"sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897",
"sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a",
"sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d",
"sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14",
"sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912",
"sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354",
"sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f",
"sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c",
"sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d",
"sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862",
"sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969",
"sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e",
"sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8",
"sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e",
"sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa",
"sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45",
"sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a",
"sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147",
"sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3",
"sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3",
"sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324",
"sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3",
"sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33",
"sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f",
"sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f",
"sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764",
"sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1",
"sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114",
"sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581",
"sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d",
"sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae",
"sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da",
"sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2",
"sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e",
"sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda",
"sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5",
"sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa",
"sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1",
"sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e",
"sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7",
"sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1",
"sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95",
"sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93",
"sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5",
"sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b",
"sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05",
"sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5",
"sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f",
"sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7",
"sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8",
"sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea",
"sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa",
"sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd",
"sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b",
"sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e",
"sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4",
"sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204",
"sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"
"sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04",
"sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0",
"sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739",
"sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a",
"sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1",
"sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218",
"sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9",
"sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188",
"sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138",
"sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585",
"sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637",
"sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe",
"sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d",
"sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1",
"sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095",
"sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9",
"sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81",
"sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57",
"sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536",
"sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a",
"sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052",
"sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01",
"sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98",
"sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433",
"sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1",
"sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f",
"sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4",
"sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b",
"sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6",
"sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8",
"sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5",
"sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306",
"sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5",
"sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f",
"sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4",
"sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be",
"sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919",
"sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af",
"sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66",
"sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1",
"sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af",
"sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec",
"sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b",
"sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289",
"sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a",
"sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d",
"sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102",
"sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9",
"sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc",
"sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45",
"sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa",
"sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a",
"sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c",
"sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461",
"sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708",
"sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca",
"sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd",
"sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913",
"sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da",
"sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0",
"sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5",
"sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5",
"sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96",
"sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41",
"sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3",
"sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456",
"sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c",
"sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867",
"sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0",
"sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213",
"sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619",
"sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240",
"sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c",
"sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377",
"sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b",
"sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c",
"sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54",
"sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b",
"sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53",
"sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029",
"sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6",
"sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885",
"sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94",
"sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134",
"sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8",
"sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9",
"sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863",
"sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b",
"sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806",
"sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11",
"sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9",
"sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817",
"sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95",
"sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8",
"sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc",
"sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47",
"sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b",
"sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0",
"sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a",
"sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f",
"sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56",
"sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef",
"sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851",
"sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7",
"sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62",
"sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4",
"sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a",
"sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c",
"sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533",
"sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f",
"sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e",
"sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a",
"sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3",
"sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b",
"sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4",
"sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0",
"sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d",
"sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3",
"sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5",
"sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534",
"sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4",
"sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144",
"sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd",
"sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd",
"sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860",
"sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704",
"sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8",
"sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d",
"sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9",
"sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f",
"sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad",
"sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc",
"sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510",
"sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937",
"sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a",
"sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460",
"sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85",
"sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86",
"sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0",
"sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246",
"sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7",
"sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa",
"sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08",
"sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270",
"sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a",
"sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169",
"sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e",
"sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75",
"sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd",
"sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354",
"sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c",
"sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1",
"sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb",
"sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f",
"sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"
],
"markers": "python_version >= '3.6'",
"version": "==5.1.0"
"version": "==5.2.1"
},
"mako": {
"hashes": [
"sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e",
"sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"
"sha256:5324b88089a8978bf76d1629774fcc2f1c07b82acdf00f4c5dd8ceadfffc4b40",
"sha256:e16c01d9ab9c11f7290eef1cfefc093fb5a45ee4a3da09e2fec2e4d1bae54e73"
],
"markers": "python_version >= '3.8'",
"version": "==1.3.2"
"version": "==1.3.3"
},
"markupsafe": {
"hashes": [
@ -748,10 +825,10 @@
},
"phonenumberslite": {
"hashes": [
"sha256:4d92f4f9079bb83588dde45fd8a414bc13e4962886aa4d23576984196f4d83c2",
"sha256:7426bc46af3de5a800a4c8f33ab13e33225d2c8ed4fc52aa3c0380dadd8d7381"
"sha256:343b300d9c8ac4dca84e6b922ec51c3d838f2feabf9dd2418da64b639d220879",
"sha256:64b513134b785fbeeaf4cc020e18d384541c4118ed3ece2118437d996f435ca0"
],
"version": "==8.13.33"
"version": "==8.13.35"
},
"psycopg2-binary": {
"hashes": [
@ -834,10 +911,11 @@
},
"pycparser": {
"hashes": [
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
"sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6",
"sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"
],
"version": "==2.21"
"markers": "python_version >= '3.8'",
"version": "==2.22"
},
"pycryptodomex": {
"hashes": [
@ -880,96 +958,96 @@
},
"pydantic": {
"hashes": [
"sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6",
"sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"
"sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352",
"sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"
],
"markers": "python_version >= '3.8'",
"version": "==2.6.4"
"version": "==2.7.0"
},
"pydantic-core": {
"hashes": [
"sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a",
"sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed",
"sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979",
"sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff",
"sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5",
"sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45",
"sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340",
"sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad",
"sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23",
"sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6",
"sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7",
"sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241",
"sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda",
"sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187",
"sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba",
"sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c",
"sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2",
"sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c",
"sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132",
"sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf",
"sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972",
"sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db",
"sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade",
"sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4",
"sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8",
"sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f",
"sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9",
"sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48",
"sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec",
"sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d",
"sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9",
"sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb",
"sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4",
"sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89",
"sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c",
"sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9",
"sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da",
"sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac",
"sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b",
"sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf",
"sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e",
"sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137",
"sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1",
"sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b",
"sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8",
"sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e",
"sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053",
"sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01",
"sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe",
"sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd",
"sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805",
"sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183",
"sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8",
"sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99",
"sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820",
"sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074",
"sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256",
"sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8",
"sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975",
"sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad",
"sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e",
"sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca",
"sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df",
"sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b",
"sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a",
"sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a",
"sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721",
"sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a",
"sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f",
"sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2",
"sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97",
"sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6",
"sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed",
"sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc",
"sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1",
"sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe",
"sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120",
"sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f",
"sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"
"sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6",
"sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb",
"sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0",
"sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6",
"sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47",
"sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a",
"sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a",
"sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac",
"sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88",
"sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db",
"sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d",
"sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d",
"sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9",
"sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e",
"sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b",
"sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d",
"sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649",
"sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c",
"sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1",
"sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09",
"sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0",
"sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90",
"sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d",
"sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294",
"sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144",
"sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b",
"sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1",
"sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b",
"sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2",
"sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad",
"sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622",
"sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17",
"sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06",
"sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc",
"sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50",
"sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d",
"sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59",
"sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539",
"sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a",
"sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b",
"sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5",
"sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9",
"sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278",
"sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6",
"sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44",
"sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0",
"sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb",
"sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80",
"sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5",
"sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570",
"sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b",
"sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de",
"sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6",
"sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8",
"sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203",
"sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7",
"sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048",
"sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae",
"sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89",
"sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f",
"sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926",
"sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2",
"sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76",
"sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d",
"sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411",
"sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9",
"sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2",
"sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586",
"sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35",
"sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c",
"sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143",
"sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6",
"sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60",
"sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b",
"sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226",
"sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519",
"sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31",
"sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7",
"sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"
],
"markers": "python_version >= '3.8'",
"version": "==2.16.3"
"version": "==2.18.1"
},
"pydantic-settings": {
"hashes": [
@ -1030,11 +1108,11 @@
},
"setuptools": {
"hashes": [
"sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e",
"sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"
"sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987",
"sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"
],
"markers": "python_version >= '3.8'",
"version": "==69.2.0"
"version": "==69.5.1"
},
"six": {
"hashes": [
@ -1046,11 +1124,11 @@
},
"sqlparse": {
"hashes": [
"sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3",
"sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"
"sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93",
"sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.4"
"markers": "python_version >= '3.8'",
"version": "==0.5.0"
},
"tblib": {
"hashes": [
@ -1063,12 +1141,12 @@
},
"typing-extensions": {
"hashes": [
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
"sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
"sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.10.0"
"version": "==4.11.0"
},
"urllib3": {
"hashes": [
@ -1097,45 +1175,45 @@
},
"zope.interface": {
"hashes": [
"sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe",
"sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac",
"sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad",
"sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b",
"sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000",
"sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328",
"sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565",
"sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f",
"sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70",
"sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037",
"sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b",
"sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab",
"sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85",
"sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099",
"sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5",
"sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef",
"sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c",
"sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd",
"sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48",
"sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd",
"sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550",
"sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797",
"sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe",
"sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d",
"sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e",
"sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1",
"sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0",
"sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532",
"sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f",
"sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f",
"sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3",
"sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a",
"sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000",
"sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e",
"sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce",
"sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"
"sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e",
"sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf",
"sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130",
"sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86",
"sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1",
"sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e",
"sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5",
"sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c",
"sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92",
"sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021",
"sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c",
"sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10",
"sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83",
"sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb",
"sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920",
"sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299",
"sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e",
"sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af",
"sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39",
"sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21",
"sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061",
"sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b",
"sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5",
"sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0",
"sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6",
"sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85",
"sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5",
"sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a",
"sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9",
"sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1",
"sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12",
"sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e",
"sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785",
"sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91",
"sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a",
"sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d"
],
"markers": "python_version >= '3.7'",
"version": "==6.2"
"version": "==6.3"
}
},
"develop": {
@ -1166,32 +1244,32 @@
},
"black": {
"hashes": [
"sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f",
"sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93",
"sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11",
"sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0",
"sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9",
"sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5",
"sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213",
"sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d",
"sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7",
"sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837",
"sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f",
"sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395",
"sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995",
"sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f",
"sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597",
"sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959",
"sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5",
"sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb",
"sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4",
"sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7",
"sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd",
"sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"
"sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d",
"sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd",
"sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33",
"sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965",
"sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070",
"sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397",
"sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745",
"sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1",
"sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665",
"sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436",
"sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb",
"sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e",
"sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6",
"sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702",
"sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8",
"sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8",
"sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3",
"sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad",
"sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf",
"sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e",
"sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641",
"sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==24.3.0"
"version": "==24.4.0"
},
"blinker": {
"hashes": [
@ -1203,12 +1281,12 @@
},
"boto3": {
"hashes": [
"sha256:7ce8c9a50af2f8a159a0dd86b40011d8dfdaba35005a118e51cd3ac72dc630f1",
"sha256:d786e7fbe3c4152866199786468a625dc77b9f27294cd7ad4f63cd2e0c927287"
"sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87",
"sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.34.71"
"version": "==1.34.88"
},
"boto3-mocking": {
"hashes": [
@ -1221,28 +1299,28 @@
},
"boto3-stubs": {
"hashes": [
"sha256:1be579780a39a75394db0c413594aec380afde86dbc7eb5eb086a97a25d3c995",
"sha256:c9959c48ee1b53e9d19e424898caacf365aaee34cee4c70f40692e2b47bc8099"
"sha256:23ca9e0cd0d3e7702d6631a1e94a4208a26b39fa6b12c734427e68a7fa649477",
"sha256:8f472d1bf09743c3d33304ecc8830d70ebe3ca19ac9604ae8da9af55421b0fce"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.34.71"
"version": "==1.34.88"
},
"botocore": {
"hashes": [
"sha256:3bc9e23aee73fe6f097823d61f79a8877790436038101a83fa96c7593e8109f8",
"sha256:c58f9ed71af2ea53d24146187130541222d7de8c27eb87d23f15457e7b83d88b"
"sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e",
"sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b"
],
"markers": "python_version >= '3.8'",
"version": "==1.34.71"
"version": "==1.34.88"
},
"botocore-stubs": {
"hashes": [
"sha256:0c3835c775db1387246c1ba8063b197604462fba8603d9b36b5dc60297197b2f",
"sha256:463248fd1d6e7b68a0c57bdd758d04c6bd0c5c2c3bfa81afdf9d64f0930b59bc"
"sha256:656e966ea152a4f2828892aa7a9673bc91799998f5a8efd8e8fe390f61c2f4f1",
"sha256:f55b03ae2e1706bd56299fd2975bb048f96aa49012a866e931a040a74f85c3cc"
],
"markers": "python_version >= '3.8' and python_version < '4.0'",
"version": "==1.34.69"
"version": "==1.34.88"
},
"click": {
"hashes": [
@ -1548,11 +1626,11 @@
},
"sqlparse": {
"hashes": [
"sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3",
"sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"
"sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93",
"sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.4"
"markers": "python_version >= '3.8'",
"version": "==0.5.0"
},
"stevedore": {
"hashes": [
@ -1572,11 +1650,11 @@
},
"types-awscrt": {
"hashes": [
"sha256:61811bbf4de95248939f9276a434be93d2b95f6ccfe8aa94e56999e9778cfcc2",
"sha256:79d5bfb01f64701b6cf442e89a37d9c4dc6dbb79a46f2f611739b2418d30ecfd"
"sha256:3ae374b553e7228ba41a528cf42bd0b2ad7303d806c73eff4aaaac1515e3ea4e",
"sha256:64898a2f4a2468f66233cb8c29c5f66de907cf80ba1ef5bb1359aef2f81bb521"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.20.5"
"version": "==0.20.9"
},
"types-cachetools": {
"hashes": [
@ -1589,11 +1667,11 @@
},
"types-pytz": {
"hashes": [
"sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3",
"sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e"
"sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981",
"sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659"
],
"markers": "python_version >= '3.8'",
"version": "==2024.1.0.20240203"
"version": "==2024.1.0.20240417"
},
"types-pyyaml": {
"hashes": [
@ -1605,29 +1683,29 @@
},
"types-requests": {
"hashes": [
"sha256:47872893d65a38e282ee9f277a4ee50d1b28bd592040df7d1fdaffdf3779937d",
"sha256:b1c1b66abfb7fa79aae09097a811c4aa97130eb8831c60e47aee4ca344731ca5"
"sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1",
"sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.31.0.20240311"
"version": "==2.31.0.20240406"
},
"types-s3transfer": {
"hashes": [
"sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69",
"sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f"
"sha256:02154cce46528287ad76ad1a0153840e0492239a0887e8833466eccf84b98da0",
"sha256:49a7c81fa609ac1532f8de3756e64b58afcecad8767933310228002ec7adff74"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.10.0"
"markers": "python_version >= '3.8' and python_version < '4.0'",
"version": "==0.10.1"
},
"typing-extensions": {
"hashes": [
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
"sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
"sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.10.0"
"version": "==4.11.0"
},
"urllib3": {
"hashes": [

View file

@ -108,7 +108,7 @@ services:
- pa11y
owasp:
image: owasp/zap2docker-stable
image: ghcr.io/zaproxy/zaproxy:stable
command: zap-baseline.py -t http://app:8080 -c zap.conf -I -r zap_report.html
volumes:
- .:/zap/wrk/

View file

@ -48,6 +48,34 @@ class MyUserAdminForm(UserChangeForm):
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
}
def __init__(self, *args, **kwargs):
"""Custom init to modify the user form"""
super(MyUserAdminForm, self).__init__(*args, **kwargs)
self._override_base_help_texts()
def _override_base_help_texts(self):
"""
Used to override pre-existing help texts in AbstractUser.
This is done to avoid modifying the base AbstractUser class.
"""
is_superuser = self.fields.get("is_superuser")
is_staff = self.fields.get("is_staff")
password = self.fields.get("password")
if is_superuser is not None:
is_superuser.help_text = "For development purposes only; provides superuser access on the database level."
if is_staff is not None:
is_staff.help_text = "Designates whether the user can log in to this admin site."
if password is not None:
# Link is copied from the base implementation of UserChangeForm.
link = f"../../{self.instance.pk}/password/"
password.help_text = (
"Raw passwords are not stored, so they will not display here. "
f'You can change the password using <a href="{link}">this form</a>.'
)
class DomainInformationAdminForm(forms.ModelForm):
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
@ -533,7 +561,7 @@ class MyUserAdmin(BaseUserAdmin):
analyst_fieldsets = (
(
None,
{"fields": ("password", "status")},
{"fields": ("status",)},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
@ -559,7 +587,6 @@ class MyUserAdmin(BaseUserAdmin):
# NOT all fields are readonly for admin, otherwise we would have
# set this at the permissions level. The exception is 'status'
analyst_readonly_fields = [
"password",
"Personal Info",
"first_name",
"last_name",
@ -974,9 +1001,10 @@ class DomainInformationAdmin(ListHeaderAdmin):
},
),
(
"More details",
"Show details",
{
"classes": ["collapse"],
"classes": ["collapse--dotgov"],
"description": "Extends type of organization",
"fields": [
"federal_type",
# TODO 1975 BEFORE MERGING: COMMENT BELOW OUT
@ -1000,9 +1028,10 @@ class DomainInformationAdmin(ListHeaderAdmin):
},
),
(
"More details",
"Show details",
{
"classes": ["collapse"],
"classes": ["collapse--dotgov"],
"description": "Extends organization name and mailing address",
"fields": [
"address_line1",
"address_line2",
@ -1203,7 +1232,17 @@ class DomainRequestAdmin(ListHeaderAdmin):
},
),
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
(
"Contacts",
{
"fields": [
"authorizing_official",
"other_contacts",
"no_other_contacts_rationale",
"cisa_representative_email",
]
},
),
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
(
"Type of organization",
@ -1216,9 +1255,10 @@ class DomainRequestAdmin(ListHeaderAdmin):
},
),
(
"More details",
"Show details",
{
"classes": ["collapse"],
"classes": ["collapse--dotgov"],
"description": "Extends type of organization",
"fields": [
"federal_type",
# TODO 1975 BEFORE MERGING: COMMENT BELOW OUT
@ -1242,9 +1282,10 @@ class DomainRequestAdmin(ListHeaderAdmin):
},
),
(
"More details",
"Show details",
{
"classes": ["collapse"],
"classes": ["collapse--dotgov"],
"description": "Extends organization name and mailing address",
"fields": [
"address_line1",
"address_line2",
@ -1277,6 +1318,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
"cisa_representative_email",
]
autocomplete_fields = [
"approved_domain",
@ -1724,21 +1766,27 @@ class DomainAdmin(ListHeaderAdmin):
if domain is not None and hasattr(domain, "domain_info"):
extra_context["original_object"] = domain.domain_info
extra_context["state_help_message"] = Domain.State.get_admin_help_text(domain.state)
extra_context["domain_state"] = domain.get_state_display()
# Pass in what the an extended expiration date would be for the expiration date modal
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
try:
curr_exp_date = domain.registry_expiration_date
except KeyError:
# No expiration date was found. Return none.
extra_context["extended_expiration_date"] = None
return super().changeform_view(request, object_id, form_url, extra_context)
new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
extra_context["extended_expiration_date"] = new_date
else:
extra_context["extended_expiration_date"] = None
self._set_expiration_date_context(domain, extra_context)
return super().changeform_view(request, object_id, form_url, extra_context)
def _set_expiration_date_context(self, domain, extra_context):
"""Given a domain, calculate the an extended expiration date
from the current registry expiration date."""
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
try:
curr_exp_date = domain.registry_expiration_date
except KeyError:
# No expiration date was found. Return none.
extra_context["extended_expiration_date"] = None
else:
new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
extra_context["extended_expiration_date"] = new_date
def response_change(self, request, obj):
# Create dictionary of action functions
ACTION_FUNCTIONS = {

View file

@ -457,7 +457,7 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
}
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
* status select amd to show/hide the rejection reason
* status select and to show/hide the rejection reason
*/
(function (){
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')

View file

@ -193,6 +193,65 @@ function clearValidators(el) {
toggleInputValidity(el, true);
}
/** Hookup listeners for yes/no togglers for form fields
* Parameters:
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
* - elementIdToShowIfYes: The Id of the element (eg. a div) to show if selected value of the given
* radio button is true (hides this element if false)
* - elementIdToShowIfNo: The Id of the element (eg. a div) to show if selected value of the given
* radio button is false (hides this element if true)
* **/
function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) {
// Get the radio buttons
let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]');
function handleRadioButtonChange() {
// Check the value of the selected radio button
// Attempt to find the radio button element that is checked
let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked');
// Check if the element exists before accessing its value
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
switch (selectedValue) {
case 'True':
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1);
break;
case 'False':
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2);
break;
default:
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0);
}
}
if (radioButtons.length) {
// Add event listener to each radio button
radioButtons.forEach(function (radioButton) {
radioButton.addEventListener('change', handleRadioButtonChange);
});
// initialize
handleRadioButtonChange();
}
}
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
function toggleTwoDomElements(ele1, ele2, index) {
let element1 = document.getElementById(ele1);
let element2 = document.getElementById(ele2);
if (element1 || element2) {
// Toggle display based on the index
if (element1) {element1.style.display = index === 1 ? 'block' : 'none';}
if (element2) {element2.style.display = index === 2 ? 'block' : 'none';}
}
else {
console.error('Unable to find elements to toggle');
}
}
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Event handlers.
@ -712,58 +771,41 @@ function hideDeletedForms() {
}
})();
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
function toggleTwoDomElements(ele1, ele2, index) {
let element1 = document.getElementById(ele1);
let element2 = document.getElementById(ele2);
if (element1 && element2) {
// Toggle display based on the index
element1.style.display = index === 1 ? 'block' : 'none';
element2.style.display = index === 2 ? 'block' : 'none';
} else {
console.error('One or both elements not found.');
}
}
/**
* An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
*
*/
(function otherContactsFormListener() {
// Get the radio buttons
let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]');
HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees')
})();
function handleRadioButtonChange() {
// Check the value of the selected radio button
// Attempt to find the radio button element that is checked
let radioButtonChecked = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked');
// Check if the element exists before accessing its value
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
/**
* An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly
*
*/
(function anythingElseFormListener() {
HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null)
})();
switch (selectedValue) {
case 'True':
toggleTwoDomElements('other-employees', 'no-other-employees', 1);
break;
case 'False':
toggleTwoDomElements('other-employees', 'no-other-employees', 2);
break;
default:
toggleTwoDomElements('other-employees', 'no-other-employees', 0);
/**
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
*
*/
(function nameserversFormListener() {
let isNameserversForm = document.querySelector(".nameservers-form");
if (isNameserversForm) {
let forms = document.querySelectorAll(".repeatable-form");
if (forms.length < 3) {
// Hide the delete buttons on the 2 nameservers
forms.forEach((form) => {
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
});
}
}
if (radioButtons.length) {
// Add event listener to each radio button
radioButtons.forEach(function (radioButton) {
radioButton.addEventListener('change', handleRadioButtonChange);
});
// initialize
handleRadioButtonChange();
}
})();
/**
@ -784,3 +826,11 @@ function toggleTwoDomElements(ele1, ele2, index) {
}
}
})();
/**
* An IIFE that listens to the yes/no radio buttons on the CISA representatives form and toggles form field visibility accordingly
*
*/
(function cisaRepresentativesFormListener() {
HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null)
})();

View file

@ -525,17 +525,30 @@ address.dja-address-contact-list {
}
// Collapse button styles for fieldsets
.module.collapse {
.module.collapse--dotgov {
margin-top: -35px;
padding-top: 0;
border: none;
h2 {
button {
background: none;
color: var(--body-fg)!important;
text-transform: none;
}
a {
color: var(--link-fg);
margin-top: 8px;
margin-left: 10px;
span {
text-decoration: underline;
font-size: 13px;
font-feature-settings: "kern";
font-kerning: normal;
line-height: 13px;
font-family: -apple-system, "system-ui", "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
}
}
.collapse--dotgov.collapsed .collapse-toggle--dotgov {
display: inline-block!important;
* {
display: inline-block;
}
}

View file

@ -108,12 +108,51 @@
padding: units(2) units(2) units(2) 0;
}
th:first-of-type {
padding-left: 0;
}
thead tr:first-child th:first-child {
border-top: none;
}
}
}
@media (min-width: 1040px){
.dotgov-table__domain-requests {
th:nth-of-type(1) {
width: 200px;
}
th:nth-of-type(2) {
width: 158px;
}
th:nth-of-type(3) {
width: 120px;
}
th:nth-of-type(4) {
width: 95px;
}
th:nth-of-type(5) {
width: 85px;
}
}
}
@media (min-width: 1040px){
.dotgov-table__registered-domains {
th:nth-of-type(1) {
width: 200px;
}
th:nth-of-type(2) {
width: 158px;
}
th:nth-of-type(3) {
width: 215px;
}
th:nth-of-type(4) {
width: 95px;
}
}
}

View file

@ -46,7 +46,7 @@ for step, view in [
(Step.PURPOSE, views.Purpose),
(Step.YOUR_CONTACT, views.YourContact),
(Step.OTHER_CONTACTS, views.OtherContacts),
(Step.ANYTHING_ELSE, views.AnythingElse),
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
(Step.REQUIREMENTS, views.Requirements),
(Step.REVIEW, views.Review),
]:

View file

@ -93,6 +93,12 @@ class UserFixture:
"last_name": "Chin",
"email": "szu.chin@associates.cisa.dhs.gov",
},
{
"username": "66bb1a5a-a091-4d7f-a6cf-4d772b4711c7",
"first_name": "Christina",
"last_name": "Burnett",
"email": "christina.burnett@cisa.dhs.gov",
},
{
"username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e",
"first_name": "Riley",
@ -169,6 +175,12 @@ class UserFixture:
"last_name": "Chin-Analyst",
"email": "szu.chin@ecstech.com",
},
{
"username": "22f88aa5-3b54-4b1f-9c57-201fb02ddba7",
"first_name": "Christina-Analyst",
"last_name": "Burnett-Analyst",
"email": "christina.burnett@gwe.cisa.dhs.gov",
},
{
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
"first_name": "Alex-Analyst",

View file

@ -1,15 +1,18 @@
from __future__ import annotations # allows forward references in annotations
from itertools import zip_longest
import logging
from typing import Callable
from api.views import DOMAIN_API_MESSAGES
from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator
from django.utils.safestring import mark_safe
from django.db.models.fields.related import ForeignObjectRel
from registrar.forms.utility.wizard_form_helper import (
RegistrarForm,
RegistrarFormSet,
BaseYesNoForm,
BaseDeletableRegistrarForm,
)
from registrar.models import Contact, DomainRequest, DraftDomain, Domain
from registrar.templatetags.url_helpers import public_site_url
from registrar.utility.enums import ValidationReturnType
@ -17,157 +20,6 @@ from registrar.utility.enums import ValidationReturnType
logger = logging.getLogger(__name__)
class RegistrarForm(forms.Form):
"""
A common set of methods and configuration.
The registrar's domain request is several pages of "steps".
Each step is an HTML form containing one or more Django "forms".
Subclass this class to create new forms.
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("label_suffix", "")
# save a reference to a domain request object
self.domain_request = kwargs.pop("domain_request", None)
super(RegistrarForm, self).__init__(*args, **kwargs)
def to_database(self, obj: DomainRequest | Contact):
"""
Adds this form's cleaned data to `obj` and saves `obj`.
Does nothing if form is not valid.
"""
if not self.is_valid():
return
for name, value in self.cleaned_data.items():
setattr(obj, name, value)
obj.save()
@classmethod
def from_database(cls, obj: DomainRequest | Contact | None):
"""Returns a dict of form field values gotten from `obj`."""
if obj is None:
return {}
return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
class RegistrarFormSet(forms.BaseFormSet):
"""
As with RegistrarForm, a common set of methods and configuration.
Subclass this class to create new formsets.
"""
def __init__(self, *args, **kwargs):
# save a reference to an domain_request object
self.domain_request = kwargs.pop("domain_request", None)
super(RegistrarFormSet, self).__init__(*args, **kwargs)
# quick workaround to ensure that the HTML `required`
# attribute shows up on required fields for any forms
# in the formset which have data already (stated another
# way: you can leave a form in the formset blank, but
# if you opt to fill it out, you must fill it out _right_)
for index in range(self.initial_form_count()):
self.forms[index].use_required_attribute = True
def should_delete(self, cleaned):
"""Should this entry be deleted from the database?"""
raise NotImplementedError
def pre_update(self, db_obj, cleaned):
"""Code to run before an item in the formset is saved."""
for key, value in cleaned.items():
setattr(db_obj, key, value)
def pre_create(self, db_obj, cleaned):
"""Code to run before an item in the formset is created in the database."""
return cleaned
def to_database(self, obj: DomainRequest):
"""
Adds this form's cleaned data to `obj` and saves `obj`.
Does nothing if form is not valid.
Hint: Subclass should call `self._to_database(...)`.
"""
raise NotImplementedError
def _to_database(
self,
obj: DomainRequest,
join: str,
should_delete: Callable,
pre_update: Callable,
pre_create: Callable,
):
"""
Performs the actual work of saving.
Has hooks such as `should_delete` and `pre_update` by which the
subclass can control behavior. Add more hooks whenever needed.
"""
if not self.is_valid():
return
obj.save()
query = getattr(obj, join).order_by("created_at").all() # order matters
# get the related name for the join defined for the db_obj for this form.
# the related name will be the reference on a related object back to db_obj
related_name = ""
field = obj._meta.get_field(join)
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
related_name = field.related_query_name()
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
related_name = field.related_query_name()
# the use of `zip` pairs the forms in the formset with the
# related objects gotten from the database -- there should always be
# at least as many forms as database entries: extra forms means new
# entries, but fewer forms is _not_ the correct way to delete items
# (likely a client-side error or an attempt at data tampering)
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
cleaned = post_data.cleaned_data if post_data is not None else {}
# matching database object exists, update it
if db_obj is not None and cleaned:
if should_delete(cleaned):
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
# Remove the specific relationship without deleting the object
getattr(db_obj, related_name).remove(self.domain_request)
else:
# If there are no other relationships, delete the object
db_obj.delete()
else:
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
# create a new db_obj and disconnect existing one
getattr(db_obj, related_name).remove(self.domain_request)
kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs)
else:
pre_update(db_obj, cleaned)
db_obj.save()
# no matching database object, create it
# make sure not to create a database object if cleaned has 'delete' attribute
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs)
@classmethod
def on_fetch(cls, query):
"""Code to run when fetching formset's objects from the database."""
return query.values()
@classmethod
def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable):
"""Returns a dict of form field values gotten from `obj`."""
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
class OrganizationTypeForm(RegistrarForm):
generic_org_type = forms.ChoiceField(
# use the long names in the domain request form
@ -588,28 +440,24 @@ class YourContactForm(RegistrarForm):
)
class OtherContactsYesNoForm(RegistrarForm):
def __init__(self, *args, **kwargs):
"""Extend the initialization of the form from RegistrarForm __init__"""
super().__init__(*args, **kwargs)
# set the initial value based on attributes of domain request
if self.domain_request and self.domain_request.has_other_contacts():
initial_value = True
elif self.domain_request and self.domain_request.has_rationale():
initial_value = False
class OtherContactsYesNoForm(BaseYesNoForm):
"""The yes/no field for the OtherContacts form."""
form_choices = ((True, "Yes, I can name other employees."), (False, "No. (Well ask you to explain why.)"))
field_name = "has_other_contacts"
@property
def form_is_checked(self):
"""
Determines the initial checked state of the form based on the domain_request's attributes.
"""
if self.domain_request.has_other_contacts():
return True
elif self.domain_request.has_rationale():
return False
else:
# No pre-selection for new domain requests
initial_value = None
self.fields["has_other_contacts"] = forms.TypedChoiceField(
coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None
choices=((True, "Yes, I can name other employees."), (False, "No. (Well ask you to explain why.)")),
initial=initial_value,
widget=forms.RadioSelect,
error_messages={
"required": "This question is required.",
},
)
return None
class OtherContactsForm(RegistrarForm):
@ -779,7 +627,7 @@ OtherContactsFormSet = forms.formset_factory(
)
class NoOtherContactsForm(RegistrarForm):
class NoOtherContactsForm(BaseDeletableRegistrarForm):
no_other_contacts_rationale = forms.CharField(
required=True,
# label has to end in a space to get the label_suffix to show
@ -794,59 +642,35 @@ class NoOtherContactsForm(RegistrarForm):
error_messages={"required": ("Rationale for no other employees is required.")},
)
def __init__(self, *args, **kwargs):
self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs)
def mark_form_for_deletion(self):
"""Marks no_other_contacts form for deletion.
This changes behavior of validity checks and to_database
methods."""
self.form_data_marked_for_deletion = True
def clean(self):
"""
This method overrides the default behavior for forms.
This cleans the form after field validation has already taken place.
In this override, remove errors associated with the form if form data
is marked for deletion.
"""
if self.form_data_marked_for_deletion:
# clear any errors raised by the form fields
# (before this clean() method is run, each field
# performs its own clean, which could result in
# errors that we wish to ignore at this point)
#
# NOTE: we cannot just clear() the errors list.
# That causes problems.
for field in self.fields:
if field in self.errors:
del self.errors[field]
return self.cleaned_data
def to_database(self, obj):
"""
This method overrides the behavior of RegistrarForm.
If form data is marked for deletion, set relevant fields
to None before saving.
Do nothing if form is not valid.
"""
if not self.is_valid():
return
if self.form_data_marked_for_deletion:
for field_name, _ in self.fields.items():
setattr(obj, field_name, None)
else:
for name, value in self.cleaned_data.items():
setattr(obj, name, value)
obj.save()
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
cisa_representative_email = forms.EmailField(
required=True,
max_length=None,
label="Your representatives email",
validators=[
MaxLengthValidator(
320,
message="Response must be less than 320 characters.",
)
],
error_messages={
"invalid": ("Enter your email address in the required format, like name@example.com."),
"required": ("Enter the email address of your CISA regional representative."),
},
)
class AnythingElseForm(RegistrarForm):
class CisaRepresentativeYesNoForm(BaseYesNoForm):
"""Yes/no toggle for the CISA regions question on additional details"""
form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) # type: ignore
field_name = "has_cisa_representative"
class AdditionalDetailsForm(BaseDeletableRegistrarForm):
anything_else = forms.CharField(
required=False,
required=True,
label="Anything else?",
widget=forms.Textarea(),
validators=[
@ -855,9 +679,22 @@ class AnythingElseForm(RegistrarForm):
message="Response must be less than 2000 characters.",
)
],
error_messages={
"required": (
"Provide additional details youd like us to know. " "If you have nothing to add, select “No.”"
)
},
)
class AdditionalDetailsYesNoForm(BaseYesNoForm):
"""Yes/no toggle for the anything else question on additional details"""
# Note that these can be set as functions/init if you need more fine-grained control.
form_is_checked = property(lambda self: self.domain_request.has_anything_else_text) # type: ignore
field_name = "has_anything_else_text"
class RequirementsForm(RegistrarForm):
is_policy_acknowledged = forms.BooleanField(
label="I read and agree to the requirements for operating a .gov domain.",

View file

@ -0,0 +1,280 @@
"""Containers helpers and base classes for the domain_request_wizard.py file"""
from itertools import zip_longest
from typing import Callable
from django.db.models.fields.related import ForeignObjectRel
from django import forms
from registrar.models import DomainRequest, Contact
class RegistrarForm(forms.Form):
"""
A common set of methods and configuration.
The registrar's domain request is several pages of "steps".
Each step is an HTML form containing one or more Django "forms".
Subclass this class to create new forms.
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("label_suffix", "")
# save a reference to a domain request object
self.domain_request = kwargs.pop("domain_request", None)
super(RegistrarForm, self).__init__(*args, **kwargs)
def to_database(self, obj: DomainRequest | Contact):
"""
Adds this form's cleaned data to `obj` and saves `obj`.
Does nothing if form is not valid.
"""
if not self.is_valid():
return
for name, value in self.cleaned_data.items():
setattr(obj, name, value)
obj.save()
@classmethod
def from_database(cls, obj: DomainRequest | Contact | None):
"""Returns a dict of form field values gotten from `obj`."""
if obj is None:
return {}
return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
class RegistrarFormSet(forms.BaseFormSet):
"""
As with RegistrarForm, a common set of methods and configuration.
Subclass this class to create new formsets.
"""
def __init__(self, *args, **kwargs):
# save a reference to an domain_request object
self.domain_request = kwargs.pop("domain_request", None)
super(RegistrarFormSet, self).__init__(*args, **kwargs)
# quick workaround to ensure that the HTML `required`
# attribute shows up on required fields for any forms
# in the formset which have data already (stated another
# way: you can leave a form in the formset blank, but
# if you opt to fill it out, you must fill it out _right_)
for index in range(self.initial_form_count()):
self.forms[index].use_required_attribute = True
def should_delete(self, cleaned):
"""Should this entry be deleted from the database?"""
raise NotImplementedError
def pre_update(self, db_obj, cleaned):
"""Code to run before an item in the formset is saved."""
for key, value in cleaned.items():
setattr(db_obj, key, value)
def pre_create(self, db_obj, cleaned):
"""Code to run before an item in the formset is created in the database."""
return cleaned
def to_database(self, obj: DomainRequest):
"""
Adds this form's cleaned data to `obj` and saves `obj`.
Does nothing if form is not valid.
Hint: Subclass should call `self._to_database(...)`.
"""
raise NotImplementedError
def _to_database(
self,
obj: DomainRequest,
join: str,
should_delete: Callable,
pre_update: Callable,
pre_create: Callable,
):
"""
Performs the actual work of saving.
Has hooks such as `should_delete` and `pre_update` by which the
subclass can control behavior. Add more hooks whenever needed.
"""
if not self.is_valid():
return
obj.save()
query = getattr(obj, join).order_by("created_at").all() # order matters
# get the related name for the join defined for the db_obj for this form.
# the related name will be the reference on a related object back to db_obj
related_name = ""
field = obj._meta.get_field(join)
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
related_name = field.related_query_name()
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
related_name = field.related_query_name()
# the use of `zip` pairs the forms in the formset with the
# related objects gotten from the database -- there should always be
# at least as many forms as database entries: extra forms means new
# entries, but fewer forms is _not_ the correct way to delete items
# (likely a client-side error or an attempt at data tampering)
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
cleaned = post_data.cleaned_data if post_data is not None else {}
# matching database object exists, update it
if db_obj is not None and cleaned:
if should_delete(cleaned):
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
# Remove the specific relationship without deleting the object
getattr(db_obj, related_name).remove(self.domain_request)
else:
# If there are no other relationships, delete the object
db_obj.delete()
else:
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
# create a new db_obj and disconnect existing one
getattr(db_obj, related_name).remove(self.domain_request)
kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs)
else:
pre_update(db_obj, cleaned)
db_obj.save()
# no matching database object, create it
# make sure not to create a database object if cleaned has 'delete' attribute
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs)
@classmethod
def on_fetch(cls, query):
"""Code to run when fetching formset's objects from the database."""
return query.values()
@classmethod
def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable):
"""Returns a dict of form field values gotten from `obj`."""
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
class BaseDeletableRegistrarForm(RegistrarForm):
"""Adds special validation and delete functionality.
Used by forms that are tied to a Yes/No form."""
def __init__(self, *args, **kwargs):
self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs)
def mark_form_for_deletion(self):
"""Marks this form for deletion.
This changes behavior of validity checks and to_database
methods."""
self.form_data_marked_for_deletion = True
def clean(self):
"""
This method overrides the default behavior for forms.
This cleans the form after field validation has already taken place.
In this override, remove errors associated with the form if form data
is marked for deletion.
"""
if self.form_data_marked_for_deletion:
# clear any errors raised by the form fields
# (before this clean() method is run, each field
# performs its own clean, which could result in
# errors that we wish to ignore at this point)
#
# NOTE: we cannot just clear() the errors list.
# That causes problems.
for field in self.fields:
if field in self.errors:
del self.errors[field]
return self.cleaned_data
def to_database(self, obj):
"""
This method overrides the behavior of RegistrarForm.
If form data is marked for deletion, set relevant fields
to None before saving.
Do nothing if form is not valid.
"""
if not self.is_valid():
return
if self.form_data_marked_for_deletion:
for field_name, _ in self.fields.items():
setattr(obj, field_name, None)
else:
for name, value in self.cleaned_data.items():
setattr(obj, name, value)
obj.save()
class BaseYesNoForm(RegistrarForm):
"""
Base class used for forms with a yes/no form with a hidden input on toggle.
Use this class when you need something similar to the AdditionalDetailsYesNoForm.
Attributes:
form_is_checked (bool): Determines the default state (checked or not) of the Yes/No toggle.
field_name (str): Specifies the form field name that the Yes/No toggle controls.
required_error_message (str): Custom error message displayed when the field is required but not provided.
form_choices (tuple): Defines the choice options for the form field, defaulting to Yes/No choices.
Usage:
Subclass this form to implement specific Yes/No fields in various parts of the application, customizing
`form_is_checked` and `field_name` as necessary for the context.
"""
form_is_checked: bool
# What field does the yes/no button hook to?
# For instance, this could be "has_other_contacts"
field_name: str
required_error_message = "This question is required."
# Default form choice mapping. Default is suitable for most cases.
form_choices = ((True, "Yes"), (False, "No"))
def __init__(self, *args, **kwargs):
"""Extend the initialization of the form from RegistrarForm __init__"""
super().__init__(*args, **kwargs)
self.fields[self.field_name] = self.get_typed_choice_field()
def get_typed_choice_field(self):
"""
Creates a TypedChoiceField for the form with specified initial value and choices.
Returns:
TypedChoiceField: A Django form field specifically configured for selecting between
predefined choices with type coercion and custom error messages.
"""
choice_field = forms.TypedChoiceField(
coerce=lambda x: x.lower() == "true" if x is not None else None,
choices=self.form_choices,
initial=self.get_initial_value(),
widget=forms.RadioSelect,
error_messages={
"required": self.required_error_message,
},
)
return choice_field
def get_initial_value(self):
"""
Determines the initial value for TypedChoiceField.
More directly, this controls the "initial" field on forms.TypedChoiceField.
Returns:
bool | None: The initial value for the form field. If the domain request is set,
this will always return the value of self.form_is_checked.
Otherwise, None will be returned as a new domain request can't start out checked.
"""
# No pre-selection for new domain requests
initial_value = self.form_is_checked if self.domain_request else None
return initial_value

View file

@ -0,0 +1,47 @@
# Generated by Django 4.2.10 on 2024-04-25 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0087_alter_domain_deleted_alter_domain_expiration_date_and_more"),
]
operations = [
migrations.AddField(
model_name="domaininformation",
name="cisa_representative_email",
field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"),
),
migrations.AddField(
model_name="domainrequest",
name="cisa_representative_email",
field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"),
),
migrations.AddField(
model_name="domainrequest",
name="has_anything_else_text",
field=models.BooleanField(
blank=True, help_text="Determines if the user has a anything_else or not", null=True
),
),
migrations.AddField(
model_name="domainrequest",
name="has_cisa_representative",
field=models.BooleanField(
blank=True, help_text="Determines if the user has a representative email or not", null=True
),
),
migrations.AlterField(
model_name="domaininformation",
name="anything_else",
field=models.TextField(blank=True, null=True, verbose_name="Additional details"),
),
migrations.AlterField(
model_name="domainrequest",
name="anything_else",
field=models.TextField(blank=True, null=True, verbose_name="Additional details"),
),
]

View file

@ -159,6 +159,31 @@ class Domain(TimeStampedModel, DomainHelper):
return help_texts.get(state, "")
@classmethod
def get_admin_help_text(cls, state):
"""Returns a help message for a desired state for /admin. If none is found, an empty string is returned"""
admin_help_texts = {
cls.UNKNOWN: (
"The creator of the associated domain request has not logged in to "
"manage the domain since it was approved. "
'The state will switch to "DNS needed" after they access the domain in the registrar.'
),
cls.DNS_NEEDED: (
"Before this domain can be used, name server addresses need to be added within the registrar."
),
cls.READY: "This domain has name servers and is ready for use.",
cls.ON_HOLD: (
"While on hold, this domain won't resolve in DNS and "
"any infrastructure (like websites) will be offline."
),
cls.DELETED: (
"This domain was permanently removed from the registry. "
"The domain no longer resolves in DNS and any infrastructure (like websites) is offline."
),
}
return admin_help_texts.get(state, "")
class Cache(property):
"""
Python descriptor to turn class methods into properties.
@ -992,22 +1017,25 @@ class Domain(TimeStampedModel, DomainHelper):
blank=False,
default=None, # prevent saving without a value
unique=True,
verbose_name="domain",
help_text="Fully qualified domain name",
verbose_name="domain",
)
state = FSMField(
max_length=21,
choices=State.choices,
default=State.UNKNOWN,
protected=True, # cannot change state directly, particularly in Django admin
# cannot change state directly, particularly in Django admin
protected=True,
# This must be defined for custom state help messages,
# as otherwise the view will purge the help field as it does not exist.
help_text=" ",
verbose_name="domain state",
help_text="Very basic info about the lifecycle of this domain object",
)
expiration_date = DateField(
null=True,
help_text=("Duplication of registry's expiration date saved for ease of reporting"),
help_text=("Date the domain expires in the registry"),
)
security_contact_registry_id = TextField(
@ -1019,15 +1047,15 @@ class Domain(TimeStampedModel, DomainHelper):
deleted = DateField(
null=True,
editable=False,
help_text='Will appear blank unless the domain is in "deleted" state',
verbose_name="deleted on",
help_text="Deleted at date",
)
first_ready = DateField(
null=True,
editable=False,
help_text='Date when this domain first moved into "ready" state; date will never change',
verbose_name="first ready on",
help_text="The last time this domain moved into the READY state",
)
def isActive(self):

View file

@ -47,6 +47,7 @@ class DomainInformation(TimeStampedModel):
"registrar.User",
on_delete=models.PROTECT,
related_name="information_created",
help_text="Person who submitted the domain request",
)
domain_request = models.OneToOneField(
@ -55,7 +56,7 @@ class DomainInformation(TimeStampedModel):
blank=True,
null=True,
related_name="DomainRequest_info",
help_text="Associated domain request",
help_text="Request associated with this domain",
unique=True,
)
@ -73,7 +74,6 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
verbose_name="election office",
help_text="Is your organization an election office?",
)
# TODO - Ticket #1911: stub this data from DomainRequest
@ -82,30 +82,26 @@ class DomainInformation(TimeStampedModel):
choices=DomainRequest.OrgChoicesElectionOffice.choices,
null=True,
blank=True,
help_text="Type of organization - Election office",
help_text='"Election" appears after the org type if it\'s an election office.',
)
federally_recognized_tribe = models.BooleanField(
null=True,
help_text="Is the tribe federally recognized",
)
state_recognized_tribe = models.BooleanField(
null=True,
help_text="Is the tribe recognized by a state",
)
tribe_name = models.CharField(
null=True,
blank=True,
help_text="Name of tribe",
)
federal_agency = models.CharField(
choices=AGENCY_CHOICES,
null=True,
blank=True,
help_text="Federal agency",
)
federal_type = models.CharField(
@ -113,38 +109,32 @@ class DomainInformation(TimeStampedModel):
choices=BranchChoices.choices,
null=True,
blank=True,
help_text="Federal government branch",
)
is_election_board = models.BooleanField(
null=True,
blank=True,
verbose_name="election office",
help_text="Is your organization an election office?",
)
organization_name = models.CharField(
null=True,
blank=True,
help_text="Organization name",
db_index=True,
)
address_line1 = models.CharField(
null=True,
blank=True,
help_text="Street address",
verbose_name="address line 1",
)
address_line2 = models.CharField(
null=True,
blank=True,
help_text="Street address line 2 (optional)",
verbose_name="address line 2",
)
city = models.CharField(
null=True,
blank=True,
help_text="City",
)
state_territory = models.CharField(
max_length=2,
@ -152,27 +142,24 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
verbose_name="state / territory",
help_text="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
help_text="Zip code",
verbose_name="zip code",
db_index=True,
verbose_name="zip code",
)
urbanization = models.CharField(
null=True,
blank=True,
help_text="Urbanization (required for Puerto Rico only)",
help_text="Required for Puerto Rico only",
verbose_name="urbanization",
)
about_your_organization = models.TextField(
null=True,
blank=True,
help_text="Information about your organization",
)
authorizing_official = models.ForeignKey(
@ -190,7 +177,6 @@ class DomainInformation(TimeStampedModel):
null=True,
# Access this information via Domain as "domain.domain_info"
related_name="domain_info",
help_text="Domain to which this information belongs",
)
# This is the contact information provided by the domain requestor. The
@ -201,6 +187,7 @@ class DomainInformation(TimeStampedModel):
blank=True,
related_name="submitted_domain_requests_information",
on_delete=models.PROTECT,
help_text='Person listed under "your contact information" in the request form',
)
purpose = models.TextField(
@ -219,13 +206,20 @@ class DomainInformation(TimeStampedModel):
no_other_contacts_rationale = models.TextField(
null=True,
blank=True,
help_text="Reason for listing no additional contacts",
help_text="Required if creator does not list other employees",
)
anything_else = models.TextField(
null=True,
blank=True,
help_text="Anything else?",
verbose_name="Additional details",
)
cisa_representative_email = models.EmailField(
null=True,
blank=True,
verbose_name="CISA regional representative",
max_length=320,
)
is_policy_acknowledged = models.BooleanField(
@ -237,7 +231,6 @@ class DomainInformation(TimeStampedModel):
notes = models.TextField(
null=True,
blank=True,
help_text="Notes about the request",
)
def __str__(self):

View file

@ -464,6 +464,7 @@ class DomainRequest(TimeStampedModel):
"registrar.User",
on_delete=models.PROTECT,
related_name="domain_requests_created",
help_text="Person who submitted the domain request; will not receive email updates",
)
investigator = models.ForeignKey(
@ -481,14 +482,12 @@ class DomainRequest(TimeStampedModel):
choices=OrganizationChoices.choices,
null=True,
blank=True,
help_text="Type of organization",
)
is_election_board = models.BooleanField(
null=True,
blank=True,
verbose_name="election office",
help_text="Is your organization an election office?",
)
# TODO - Ticket #1911: stub this data from DomainRequest
@ -497,30 +496,26 @@ class DomainRequest(TimeStampedModel):
choices=OrgChoicesElectionOffice.choices,
null=True,
blank=True,
help_text="Type of organization - Election office",
help_text='"Election" appears after the org type if it\'s an election office.',
)
federally_recognized_tribe = models.BooleanField(
null=True,
help_text="Is the tribe federally recognized",
)
state_recognized_tribe = models.BooleanField(
null=True,
help_text="Is the tribe recognized by a state",
)
tribe_name = models.CharField(
null=True,
blank=True,
help_text="Name of tribe",
)
federal_agency = models.CharField(
choices=AGENCY_CHOICES,
null=True,
blank=True,
help_text="Federal agency",
)
federal_type = models.CharField(
@ -528,32 +523,27 @@ class DomainRequest(TimeStampedModel):
choices=BranchChoices.choices,
null=True,
blank=True,
help_text="Federal government branch",
)
organization_name = models.CharField(
null=True,
blank=True,
help_text="Organization name",
db_index=True,
)
address_line1 = models.CharField(
null=True,
blank=True,
help_text="Street address",
verbose_name="Address line 1",
)
address_line2 = models.CharField(
null=True,
blank=True,
help_text="Street address line 2 (optional)",
verbose_name="Address line 2",
)
city = models.CharField(
null=True,
blank=True,
help_text="City",
)
state_territory = models.CharField(
max_length=2,
@ -561,26 +551,23 @@ class DomainRequest(TimeStampedModel):
null=True,
blank=True,
verbose_name="state / territory",
help_text="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
verbose_name="zip code",
help_text="Zip code",
db_index=True,
)
urbanization = models.CharField(
null=True,
blank=True,
help_text="Urbanization (required for Puerto Rico only)",
help_text="Required for Puerto Rico only",
)
about_your_organization = models.TextField(
null=True,
blank=True,
help_text="Information about your organization",
)
authorizing_official = models.ForeignKey(
@ -603,7 +590,7 @@ class DomainRequest(TimeStampedModel):
"Domain",
null=True,
blank=True,
help_text="The approved domain",
help_text="Domain associated with this request; will be blank until request is approved",
related_name="domain_request",
on_delete=models.SET_NULL,
)
@ -612,7 +599,6 @@ class DomainRequest(TimeStampedModel):
"DraftDomain",
null=True,
blank=True,
help_text="The requested domain",
related_name="domain_request",
on_delete=models.PROTECT,
)
@ -621,6 +607,7 @@ class DomainRequest(TimeStampedModel):
"registrar.Website",
blank=True,
related_name="alternatives+",
help_text="Other domain names the creator provided for consideration",
)
# This is the contact information provided by the domain requestor. The
@ -631,12 +618,12 @@ class DomainRequest(TimeStampedModel):
blank=True,
related_name="submitted_domain_requests",
on_delete=models.PROTECT,
help_text='Person listed under "your contact information" in the request form; will receive email updates',
)
purpose = models.TextField(
null=True,
blank=True,
help_text="Purpose of your domain",
)
other_contacts = models.ManyToManyField(
@ -649,13 +636,38 @@ class DomainRequest(TimeStampedModel):
no_other_contacts_rationale = models.TextField(
null=True,
blank=True,
help_text="Reason for listing no additional contacts",
help_text="Required if creator does not list other employees",
)
anything_else = models.TextField(
null=True,
blank=True,
help_text="Anything else?",
verbose_name="Additional details",
)
# This is a drop-in replacement for a has_anything_else_text() function.
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
# a tertiary state. We should not display this in /admin.
has_anything_else_text = models.BooleanField(
null=True,
blank=True,
help_text="Determines if the user has a anything_else or not",
)
cisa_representative_email = models.EmailField(
null=True,
blank=True,
verbose_name="CISA regional representative",
max_length=320,
)
# This is a drop-in replacement for an has_cisa_representative() function.
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
# a tertiary state. We should not display this in /admin.
has_cisa_representative = models.BooleanField(
null=True,
blank=True,
help_text="Determines if the user has a representative email or not",
)
is_policy_acknowledged = models.BooleanField(
@ -676,7 +688,6 @@ class DomainRequest(TimeStampedModel):
notes = models.TextField(
null=True,
blank=True,
help_text="Notes about this request",
)
def sync_organization_type(self):
@ -711,8 +722,33 @@ class DomainRequest(TimeStampedModel):
def save(self, *args, **kwargs):
"""Save override for custom properties"""
self.sync_organization_type()
self.sync_yes_no_form_fields()
super().save(*args, **kwargs)
def sync_yes_no_form_fields(self):
"""Some yes/no forms use a db field to track whether it was checked or not.
We handle that here for def save().
"""
# This ensures that if we have prefilled data, the form is prepopulated
if self.cisa_representative_email is not None:
self.has_cisa_representative = self.cisa_representative_email != ""
# This check is required to ensure that the form doesn't start out checked
if self.has_cisa_representative is not None:
self.has_cisa_representative = (
self.cisa_representative_email != "" and self.cisa_representative_email is not None
)
# This ensures that if we have prefilled data, the form is prepopulated
if self.anything_else is not None:
self.has_anything_else_text = self.anything_else != ""
# This check is required to ensure that the form doesn't start out checked.
if self.has_anything_else_text is not None:
self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None
def __str__(self):
try:
if self.requested_domain and self.requested_domain.name:
@ -1051,6 +1087,16 @@ class DomainRequest(TimeStampedModel):
"""Does this domain request have other contacts listed?"""
return self.other_contacts.exists()
def has_additional_details(self) -> bool:
"""Combines the has_anything_else_text and has_cisa_representative fields,
then returns if this domain request has either of them."""
# Split out for linter
has_details = False
if self.has_anything_else_text or self.has_cisa_representative:
has_details = True
return has_details
def is_federal(self) -> Union[bool, None]:
"""Is this domain request for a federal agency?

View file

@ -22,14 +22,13 @@ class Host(TimeStampedModel):
default=None, # prevent saving without a value
unique=False,
verbose_name="host name",
help_text="Fully qualified domain name",
)
domain = models.ForeignKey(
"registrar.Domain",
on_delete=models.PROTECT,
related_name="host", # access this Host via the Domain as `domain.host`
help_text="Domain to which this host belongs",
help_text="Domain associated with this host",
)
def __str__(self):

View file

@ -21,12 +21,11 @@ class HostIP(TimeStampedModel):
default=None, # prevent saving without a value
validators=[validate_ipv46_address],
verbose_name="IP address",
help_text="IP address",
)
host = models.ForeignKey(
"registrar.Host",
on_delete=models.PROTECT,
related_name="ip", # access this HostIP via the Host as `host.ip`
help_text="Host to which this IP address belongs",
help_text="IP associated with this host",
)

View file

@ -34,6 +34,7 @@ class User(AbstractUser):
null=True, # Allow the field to be null
blank=True, # Allow the field to be blank
verbose_name="user status",
help_text='Users in "restricted" status cannot make updates in the registrar or start a new request.',
)
domains = models.ManyToManyField(

View file

@ -164,7 +164,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# There is no avenue for this to occur in the UI,
# as such - this can only occur if the object is initialized in this way.
# Or if there are pre-existing data.
logger.warning(
logger.debug(
"create_or_update_organization_type() -> is_election_board "
f"cannot exist for {generic_org_type}. Setting to None."
)

View file

@ -9,7 +9,6 @@ class VerifiedByStaff(TimeStampedModel):
email = models.EmailField(
null=False,
blank=False,
help_text="Email",
db_index=True,
)
@ -19,12 +18,12 @@ class VerifiedByStaff(TimeStampedModel):
blank=True,
on_delete=models.SET_NULL,
related_name="verifiedby_user",
help_text="Person who verified this user",
)
notes = models.TextField(
null=False,
blank=False,
help_text="Notes",
)
class Meta:

View file

@ -12,7 +12,7 @@ class Website(TimeStampedModel):
website = models.CharField(
max_length=255,
null=False,
help_text="",
help_text="An alternative domain or current website listed on a domain request",
)
def __str__(self) -> str:

View file

@ -23,6 +23,7 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
<script type="application/javascript" src="{% static 'js/get-gov-reports.js' %}" defer></script>
<script type="application/javascript" src="{% static 'js/dja-collapse.js' %}" defer></script>
{% endblock %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}

View file

@ -6,9 +6,23 @@ It is not inherently customizable on its own, so we can modify this instead.
https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/includes/fieldset.html
{% endcomment %}
<fieldset class="module aligned {{ fieldset.classes }}">
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
{% if fieldset.name %}
{# Customize the markup for the collapse toggle #}
{% if 'collapse--dotgov' in fieldset.classes %}
<button type="button">
<span>{{ fieldset.name }}</span>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#expand_more"></use>
</svg>
</button>
<legend class="sr-only">{{ fieldset.description }}</legend>
{% else %}
<h2>{{ fieldset.name }}</h2>
{% endif %}
{% endif %}
{% if fieldset.description %}
{# Customize the markup for the collapse toggle: Do not show a description for the collapse fieldsets, instead we're using the description as a screen reader only legend #}
{% if fieldset.description and 'collapse--dotgov' not in fieldset.classes %}
<div class="description">{{ fieldset.description|safe }}</div>
{% endif %}

View file

@ -33,7 +33,10 @@
{% endif %}
</div>
</div>
{{ block.super }}
{% for fieldset in adminform %}
{% include "django/admin/includes/domain_fieldset.html" with state_help_message=state_help_message %}
{% endfor %}
{% endblock %}
{% block submit_buttons_bottom %}

View file

@ -65,9 +65,16 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endwith %}
{% endblock field_readonly %}
{% block help_text %}
<div class="help margin-bottom-1" {% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div>{{ field.field.help_text|safe }}</div>
</div>
{% endblock help_text %}
{% block after_help_text %}
{% if field.field.name == "creator" %}
<div class="flex-container">
<div class="flex-container tablet:margin-top-1">
<label aria-label="Creator contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
</div>

View file

@ -0,0 +1,21 @@
{% extends "admin/fieldset.html" %}
{% load static url_helpers %}
{% block field_readonly %}
{% if field.field.name == "state" %}
<div class="readonly">{{ domain_state }}</div>
{% else %}
<div class="readonly">{{ field.contents }}</div>
{% endif %}
{% endblock %}
{% block help_text %}
<div class="help margin-bottom-1" {% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
{% if field.field.name == "state" %}
<div>{{ state_help_message }}</div>
{% else %}
<div>{{ field.field.help_text|safe }}</div>
{% endif %}
</div>
{% endblock help_text %}

View file

@ -0,0 +1,55 @@
{% extends 'domain_request_form.html' %}
{% load static field_helpers %}
{% block form_instructions %}
<em>These questions are required (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
{% endblock %}
{% block form_required_fields_help_text %}
{# commented out so it does not appear at this point on this page #}
{% endblock %}
<!-- TODO-NL: (refactor) Breakup into two separate components-->
{% block form_fields %}
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Are you working with a CISA regional representative on your domain request?</h2>
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has <a href="https://www.cisa.gov/about/regions" target="_blank">10 regions</a> that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
</legend>
<!-- Toggle -->
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.0.has_cisa_representative %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
<!-- TODO-NL: Hookup forms.0 to yes/no form for cisa representative (backend def)-->
</fieldset>
<div id="cisa-representative" class="cisa-representative-form">
{% input_with_errors forms.1.cisa_representative_email %}
{# forms.1 is a form for inputting the e-mail of a cisa representative #}
<!-- TODO-NL: Hookup forms.1 to cisa representative form (backend def) -->
</div>
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Is there anything else youd like us to know about your domain request?</h2>
</legend>
<!-- Toggle -->
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.2.has_anything_else_text %}
{% endwith %}
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
<!-- TODO-NL: Hookup forms.2 to yes/no form for anything else form (backend def)-->
</fieldset>
<div id="anything-else">
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.3.anything_else %}
{% endwith %}
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
<!-- TODO-NL: Hookup forms.3 to anything else form (backend def) -->
</div>
{% endblock %}

View file

@ -1,19 +0,0 @@
{% extends 'domain_request_form.html' %}
{% load field_helpers %}
{% block form_instructions %}
<h2>Is there anything else youd like us to know about your domain request?</h2>
<p>This question is optional.</p>
{% endblock %}
{% block form_required_fields_help_text %}
{# commented out so it does not appear on this page #}
{% endblock %}
{% block form_fields %}
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.anything_else %}
{% endwith %}
{% endblock %}

View file

@ -155,11 +155,20 @@
{% endif %}
{% if step == Step.ANYTHING_ELSE %}
{% if step == Step.ADDITIONAL_DETAILS %}
{% namespaced_url 'domain-request' step as domain_request_url %}
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"No" %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
{% endwith %}
<h3 class="register-form-review-header">Anything else</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% if domain_request.anything_else %}
{{domain_request.anything_else}}
{% else %}
No
{% endif %}
</ul>
{% endif %}

View file

@ -116,7 +116,18 @@
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
{% endif %}
{% include "includes/summary_item.html" with title='Anything else?' value=DomainRequest.anything_else|default:"No" heading_level=heading_level %}
{# We always show this field even if None #}
{% if DomainRequest %}
{% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regional representative' value=DomainRequest.cisa_representative_email custom_text_for_value_none='No' heading_level=heading_level %}
<h3 class="register-form-review-header">Anything else</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% if DomainRequest.anything_else %}
{{DomainRequest.anything_else}}
{% else %}
No
{% endif %}
</ul>
{% endif %}
{% endwith %}
</div>

View file

@ -26,7 +26,7 @@
<section class="section--outlined">
<h2>Domains</h2>
{% if domains %}
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__registered-domains">
<caption class="sr-only">Your registered domains</caption>
<thead>
<tr>
@ -104,7 +104,7 @@
<section class="section--outlined">
<h2>Domain requests</h2>
{% if domain_requests %}
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__domain-requests">
<caption class="sr-only">Your domain requests</caption>
<thead>
<tr>

View file

@ -21,6 +21,9 @@
{% else %}
</h2>
{% endif %}
{% if sub_header_text %}
<h3 class="register-form-review-header">{{ sub_header_text }}</h3>
{% endif %}
{% if address %}
{% include "includes/organization_address.html" with organization=value %}
{% elif contact %}
@ -39,6 +42,10 @@
</dd>
{% endfor %}
</dl>
{% elif custom_text_for_value_none %}
<p>
{{ custom_text_for_value_none }}
</p>
{% else %}
<p>
None
@ -92,6 +99,8 @@
<p class="margin-top-0 margin-bottom-0">
{% if value %}
{{ value }}
{% elif custom_text_for_value_none %}
{{ custom_text_for_value_none }}
{% else %}
None
{% endif %}

View file

@ -18,6 +18,7 @@ from registrar.admin import (
AuditedAdmin,
ContactAdmin,
DomainInformationAdmin,
MyHostAdmin,
UserDomainRoleAdmin,
VerifiedByStaffAdmin,
)
@ -76,6 +77,13 @@ class TestDomainAdmin(MockEppLib, WebTest):
self.app.set_user(self.superuser.username)
self.client.force_login(self.superuser)
# Add domain data
self.ready_domain, _ = Domain.objects.get_or_create(name="fakeready.gov", state=Domain.State.READY)
self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN)
self.dns_domain, _ = Domain.objects.get_or_create(name="fakedns.gov", state=Domain.State.DNS_NEEDED)
self.hold_domain, _ = Domain.objects.get_or_create(name="fakehold.gov", state=Domain.State.ON_HOLD)
self.deleted_domain, _ = Domain.objects.get_or_create(name="fakedeleted.gov", state=Domain.State.DELETED)
# Contains some test tools
self.test_helper = GenericTestHelper(
factory=self.factory,
@ -159,6 +167,68 @@ class TestDomainAdmin(MockEppLib, WebTest):
# Test for the copy link
self.assertContains(response, "usa-button__clipboard")
@less_console_noise_decorator
def test_helper_text(self):
"""
Tests for the correct helper text on this page
"""
# Create a ready domain with a preset expiration date
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# These should exist in the response
expected_values = [
("expiration_date", "Date the domain expires in the registry"),
("first_ready_at", 'Date when this domain first moved into "ready" state; date will never change'),
("deleted_at", 'Will appear blank unless the domain is in "deleted" state'),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
@less_console_noise_decorator
def test_helper_text_state(self):
"""
Tests for the correct state helper text on this page
"""
# We don't need to check for all text content, just a portion of it
expected_unknown_domain_message = "The creator of the associated domain request has not logged in to"
expected_dns_message = "Before this domain can be used, name server addresses need"
expected_hold_message = "While on hold, this domain"
expected_deleted_message = "This domain was permanently removed from the registry."
expected_messages = [
(self.ready_domain, "This domain has name servers and is ready for use."),
(self.unknown_domain, expected_unknown_domain_message),
(self.dns_domain, expected_dns_message),
(self.hold_domain, expected_hold_message),
(self.deleted_domain, expected_deleted_message),
]
p = "adminpass"
self.client.login(username="superuser", password=p)
for domain, message in expected_messages:
with self.subTest(domain_state=domain.state):
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.id),
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Check that the right help text exists
self.assertContains(response, message)
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
def test_extend_expiration_date_button(self, mock_date_today):
"""
@ -782,6 +852,63 @@ class TestDomainRequestAdmin(MockEppLib):
)
self.mock_client = MockSESClient()
@less_console_noise_decorator
def test_helper_text(self):
"""
Tests for the correct helper text on this page
"""
# Create a fake domain request and domain
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
# These should exist in the response
expected_values = [
("creator", "Person who submitted the domain request; will not receive email updates"),
(
"submitter",
'Person listed under "your contact information" in the request form; will receive email updates',
),
("approved_domain", "Domain associated with this request; will be blank until request is approved"),
("no_other_contacts_rationale", "Required if creator does not list other employees"),
("alternative_domains", "Other domain names the creator provided for consideration"),
("no_other_contacts_rationale", "Required if creator does not list other employees"),
("Urbanization", "Required for Puerto Rico only"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
@less_console_noise_decorator
def test_collaspe_toggle_button_markup(self):
"""
Tests for the correct collapse toggle button markup
"""
# Create a fake domain request and domain
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
self.test_helper.assertContains(response, "<span>Show details</span>")
@less_console_noise_decorator
def test_analyst_can_see_and_edit_alternative_domain(self):
"""Tests if an analyst can still see and edit the alternative domain field"""
@ -1889,6 +2016,9 @@ class TestDomainRequestAdmin(MockEppLib):
"purpose",
"no_other_contacts_rationale",
"anything_else",
"has_anything_else_text",
"cisa_representative_email",
"has_cisa_representative",
"is_policy_acknowledged",
"submission_date",
"notes",
@ -1920,6 +2050,7 @@ class TestDomainRequestAdmin(MockEppLib):
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
"cisa_representative_email",
]
self.assertEqual(readonly_fields, expected_fields)
@ -2315,6 +2446,54 @@ class DomainInvitationAdminTest(TestCase):
self.assertContains(response, retrieved_html, count=1)
class TestHostAdmin(TestCase):
def setUp(self):
"""Setup environment for a mock admin user"""
super().setUp()
self.site = AdminSite()
self.factory = RequestFactory()
self.admin = MyHostAdmin(model=Host, admin_site=self.site)
self.client = Client(HTTP_HOST="localhost:8080")
self.superuser = create_superuser()
self.test_helper = GenericTestHelper(
factory=self.factory,
user=self.superuser,
admin=self.admin,
url="/admin/registrar/Host/",
model=Host,
)
def tearDown(self):
super().tearDown()
Host.objects.all().delete()
Domain.objects.all().delete()
@less_console_noise_decorator
def test_helper_text(self):
"""
Tests for the correct helper text on this page
"""
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# Create a fake host
host, _ = Host.objects.get_or_create(name="ns1.test.gov", domain=domain)
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/host/{}/change/".format(host.pk),
follow=True,
)
# Make sure the page loaded
self.assertEqual(response.status_code, 200)
# These should exist in the response
expected_values = [
("domain", "Domain associated with this host"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
class TestDomainInformationAdmin(TestCase):
def setUp(self):
"""Setup environment for a mock admin user"""
@ -2367,6 +2546,38 @@ class TestDomainInformationAdmin(TestCase):
Contact.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
def test_helper_text(self):
"""
Tests for the correct helper text on this page
"""
# Create a fake domain request and domain
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
domain_request.approve()
domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domaininformation/{}/change/".format(domain_info.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_info.domain.name)
# These should exist in the response
expected_values = [
("creator", "Person who submitted the domain request"),
("submitter", 'Person listed under "your contact information" in the request form'),
("domain_request", "Request associated with this domain"),
("no_other_contacts_rationale", "Required if creator does not list other employees"),
("urbanization", "Required for Puerto Rico only"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
@less_console_noise_decorator
def test_other_contacts_has_readonly_link(self):
"""Tests if the readonly other_contacts field has links"""
@ -2711,7 +2922,7 @@ class UserDomainRoleAdminTest(TestCase):
self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com</a></th>", count=1)
class ListHeaderAdminTest(TestCase):
class TestListHeaderAdmin(TestCase):
def setUp(self):
self.site = AdminSite()
self.factory = RequestFactory()
@ -2784,10 +2995,43 @@ class ListHeaderAdminTest(TestCase):
User.objects.all().delete()
class MyUserAdminTest(TestCase):
class TestMyUserAdmin(TestCase):
def setUp(self):
admin_site = AdminSite()
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
self.client = Client(HTTP_HOST="localhost:8080")
self.superuser = create_superuser()
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
User.objects.all().delete()
@less_console_noise_decorator
def test_helper_text(self):
"""
Tests for the correct helper text on this page
"""
user = create_user()
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/user/{}/change/".format(user.pk),
follow=True,
)
# Make sure the page loaded
self.assertEqual(response.status_code, 200)
# These should exist in the response
expected_values = [
("password", "Raw passwords are not stored, so they will not display here."),
("status", 'Users in "restricted" status cannot make updates in the registrar or start a new request.'),
("is_staff", "Designates whether the user can log in to this admin site"),
("is_superuser", "For development purposes only; provides superuser access on the database level"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
def test_list_display_without_username(self):
with less_console_noise():
@ -2809,8 +3053,9 @@ class MyUserAdminTest(TestCase):
def test_get_fieldsets_superuser(self):
with less_console_noise():
request = self.client.request().wsgi_request
request.user = create_superuser()
request.user = self.superuser
fieldsets = self.admin.get_fieldsets(request)
expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request)
self.assertEqual(fieldsets, expected_fieldsets)
@ -2820,16 +3065,13 @@ class MyUserAdminTest(TestCase):
request.user = create_user()
fieldsets = self.admin.get_fieldsets(request)
expected_fieldsets = (
(None, {"fields": ("password", "status")}),
(None, {"fields": ("status",)}),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
("Permissions", {"fields": ("is_active", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
self.assertEqual(fieldsets, expected_fieldsets)
def tearDown(self):
User.objects.all().delete()
class AuditedAdminTest(TestCase):
def setUp(self):
@ -3303,10 +3545,43 @@ class ContactAdminTest(TestCase):
User.objects.all().delete()
class VerifiedByStaffAdminTestCase(TestCase):
class TestVerifiedByStaffAdmin(TestCase):
def setUp(self):
super().setUp()
self.site = AdminSite()
self.superuser = create_superuser()
self.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=self.site)
self.factory = RequestFactory()
self.client = Client(HTTP_HOST="localhost:8080")
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
VerifiedByStaff.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
def test_helper_text(self):
"""
Tests for the correct helper text on this page
"""
vip_instance, _ = VerifiedByStaff.objects.get_or_create(email="test@example.com", notes="Test Notes")
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/verifiedbystaff/{}/change/".format(vip_instance.pk),
follow=True,
)
# Make sure the page loaded
self.assertEqual(response.status_code, 200)
# These should exist in the response
expected_values = [
("requestor", "Person who verified this user"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
def test_save_model_sets_user_field(self):
with less_console_noise():

View file

@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import (
RequirementsForm,
TribalGovernmentForm,
PurposeForm,
AnythingElseForm,
AdditionalDetailsForm,
AboutYourOrganizationForm,
)
from registrar.forms.domain import ContactForm
@ -274,7 +274,7 @@ class TestFormValidation(MockEppLib):
def test_anything_else_form_about_your_organization_character_count_invalid(self):
"""Response must be less than 2000 characters."""
form = AnythingElseForm(
form = AdditionalDetailsForm(
data={
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
"shankle, drumstick doner chicken landjaeger turkey andouille."

View file

@ -356,33 +356,39 @@ class DomainRequestTests(TestWithUser, WebTest):
# the post request should return a redirect to the next form in
# the domain request page
self.assertEqual(other_contacts_result.status_code, 302)
self.assertEqual(other_contacts_result["Location"], "/request/anything_else/")
self.assertEqual(other_contacts_result["Location"], "/request/additional_details/")
num_pages_tested += 1
# ---- ANYTHING ELSE PAGE ----
# ---- ADDITIONAL DETAILS PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
anything_else_page = other_contacts_result.follow()
anything_else_form = anything_else_page.forms[0]
additional_details_page = other_contacts_result.follow()
additional_details_form = additional_details_page.forms[0]
anything_else_form["anything_else-anything_else"] = "Nothing else."
# load inputs with test data
additional_details_form["additional_details-has_cisa_representative"] = "True"
additional_details_form["additional_details-has_anything_else_text"] = "True"
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
additional_details_form["additional_details-anything_else"] = "Nothing else."
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
anything_else_result = anything_else_form.submit()
additional_details_result = additional_details_form.submit()
# validate that data from this step are being saved
domain_request = DomainRequest.objects.get() # there's only one
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
self.assertEqual(domain_request.anything_else, "Nothing else.")
# the post request should return a redirect to the next form in
# the domain request page
self.assertEqual(anything_else_result.status_code, 302)
self.assertEqual(anything_else_result["Location"], "/request/requirements/")
self.assertEqual(additional_details_result.status_code, 302)
self.assertEqual(additional_details_result["Location"], "/request/requirements/")
num_pages_tested += 1
# ---- REQUIREMENTS PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requirements_page = anything_else_result.follow()
requirements_page = additional_details_result.follow()
requirements_form = requirements_page.forms[0]
requirements_form["requirements-is_policy_acknowledged"] = True
@ -434,6 +440,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(review_page, "Another Tester")
self.assertContains(review_page, "testy2@town.com")
self.assertContains(review_page, "(201) 555-5557")
self.assertContains(review_page, "FakeEmail@gmail.com")
self.assertContains(review_page, "Nothing else.")
# We can't test the modal itself as it relies on JS for init and triggering,
@ -717,13 +724,25 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
def test_yes_no_form_inits_blank_for_new_domain_request(self):
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
new domain requests"""
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
def test_yes_no_additional_form_inits_blank_for_new_domain_request(self):
"""On the Additional Details page, the yes/no form gets initialized with nothing selected for
new domain requests"""
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
additional_form = additional_details_page.forms[0]
# Check the cisa representative yes/no field
self.assertEquals(additional_form["additional_details-has_cisa_representative"].value, None)
# Check the anything else yes/no field
self.assertEquals(additional_form["additional_details-has_anything_else_text"].value, None)
def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self):
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
domain request has other contacts"""
@ -744,6 +763,38 @@ class DomainRequestTests(TestWithUser, WebTest):
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self):
"""On the Additional Details page, the yes/no form gets initialized with YES selected
for both yes/no radios if the domain request has a value for cisa_representative and
anything_else"""
domain_request = completed_domain_request(user=self.user, has_anything_else=True)
domain_request.cisa_representative_email = "test@igorville.gov"
domain_request.anything_else = "1234"
domain_request.save()
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_form = additional_details_page.forms[0]
# Check the cisa representative yes/no field
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
self.assertEquals(yes_no_cisa, "True")
# Check the anything else yes/no field
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
self.assertEquals(yes_no_anything_else, "True")
def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self):
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
domain request has no other contacts"""
@ -766,6 +817,230 @@ class DomainRequestTests(TestWithUser, WebTest):
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
def test_yes_no_form_for_domain_request_with_no_cisa_representative_and_anything_else(self):
"""On the Additional details page, the form preselects "no" when has_cisa_representative
and anything_else is no"""
domain_request = completed_domain_request(user=self.user, has_anything_else=False)
# Unlike the other contacts form, the no button is tracked with these boolean fields.
# This means that we should expect this to correlate with the no button.
domain_request.has_anything_else_text = False
domain_request.has_cisa_representative = False
domain_request.save()
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_form = additional_details_page.forms[0]
# Check the cisa representative yes/no field
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
self.assertEquals(yes_no_cisa, "False")
# Check the anything else yes/no field
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
self.assertEquals(yes_no_anything_else, "False")
def test_submitting_additional_details_deletes_cisa_representative_and_anything_else(self):
"""When a user submits the Additional Details form with no selected for all fields,
the domain request's data gets wiped when submitted"""
domain_request = completed_domain_request(name="nocisareps.gov", user=self.user)
domain_request.cisa_representative_email = "fake@faketown.gov"
domain_request.save()
# Make sure we have the data we need for the test
self.assertEqual(domain_request.anything_else, "There is more")
self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov")
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_form = additional_details_page.forms[0]
# Check the cisa representative yes/no field
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
self.assertEquals(yes_no_cisa, "True")
# Check the anything else yes/no field
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
self.assertEquals(yes_no_anything_else, "True")
# Set fields to false
additional_details_form["additional_details-has_cisa_representative"] = "False"
additional_details_form["additional_details-has_anything_else_text"] = "False"
# Submit the form
additional_details_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Verify that the anything_else and cisa_representative have been deleted from the DB
domain_request = DomainRequest.objects.get(requested_domain__name="nocisareps.gov")
# Check that our data has been cleared
self.assertEqual(domain_request.anything_else, None)
self.assertEqual(domain_request.cisa_representative_email, None)
# Double check the yes/no fields
self.assertEqual(domain_request.has_anything_else_text, False)
self.assertEqual(domain_request.has_cisa_representative, False)
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
"""When a user submits the Additional Details form,
the domain request's data gets submitted"""
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
# Make sure we have the data we need for the test
self.assertEqual(domain_request.anything_else, None)
self.assertEqual(domain_request.cisa_representative_email, None)
# These fields should not be selected at all, since we haven't initialized the form yet
self.assertEqual(domain_request.has_anything_else_text, None)
self.assertEqual(domain_request.has_cisa_representative, None)
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_form = additional_details_page.forms[0]
# Set fields to true, and set data on those fields
additional_details_form["additional_details-has_cisa_representative"] = "True"
additional_details_form["additional_details-has_anything_else_text"] = "True"
additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov"
additional_details_form["additional_details-anything_else"] = "redandblue"
# Submit the form
additional_details_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Verify that the anything_else and cisa_representative exist in the db
domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov")
self.assertEqual(domain_request.anything_else, "redandblue")
self.assertEqual(domain_request.cisa_representative_email, "test@faketest.gov")
self.assertEqual(domain_request.has_cisa_representative, True)
self.assertEqual(domain_request.has_anything_else_text, True)
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a cisa representative must provide a value"""
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_form = additional_details_page.forms[0]
# Set fields to true, and set data on those fields
additional_details_form["additional_details-has_cisa_representative"] = "True"
additional_details_form["additional_details-has_anything_else_text"] = "False"
# Submit the form
response = additional_details_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
self.assertContains(response, "Enter the email address of your CISA regional representative.")
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a anything else must provide a value"""
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_form = additional_details_page.forms[0]
# Set fields to true, and set data on those fields
additional_details_form["additional_details-has_cisa_representative"] = "False"
additional_details_form["additional_details-has_anything_else_text"] = "True"
# Submit the form
response = additional_details_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
expected_message = "Provide additional details youd like us to know. If you have nothing to add, select “No.”"
self.assertContains(response, expected_message)
def test_additional_details_form_fields_required(self):
"""When a user submits the Additional Details form without checking the
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
self.assertEqual(domain_request.has_anything_else_text, None)
self.assertEqual(domain_request.has_cisa_representative, None)
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
additional_details_form = additional_details_page.forms[0]
# Submit the form
response = additional_details_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# We expect to see this twice for both fields. This results in a count of 4
# due to screen reader information / html.
self.assertContains(response, "This question is required.", count=4)
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
"""When a user submits the Other Contacts form with other contacts selected, the domain request's
no other contacts rationale gets deleted"""

View file

@ -45,7 +45,7 @@ class Step(StrEnum):
PURPOSE = "purpose"
YOUR_CONTACT = "your_contact"
OTHER_CONTACTS = "other_contacts"
ANYTHING_ELSE = "anything_else"
ADDITIONAL_DETAILS = "additional_details"
REQUIREMENTS = "requirements"
REVIEW = "review"
@ -91,7 +91,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
Step.PURPOSE: _("Purpose of your domain"),
Step.YOUR_CONTACT: _("Your contact information"),
Step.OTHER_CONTACTS: _("Other employees from your organization"),
Step.ANYTHING_ELSE: _("Anything else?"),
Step.ADDITIONAL_DETAILS: _("Additional details"),
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
Step.REVIEW: _("Review and submit your domain request"),
}
@ -365,8 +365,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
self.domain_request.other_contacts.exists()
or self.domain_request.no_other_contacts_rationale is not None
),
"anything_else": (
self.domain_request.anything_else is not None or self.domain_request.is_policy_acknowledged is not None
"additional_details": (
(self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email)
or self.domain_request.is_policy_acknowledged is not None
),
"requirements": self.domain_request.is_policy_acknowledged is not None,
"review": self.domain_request.is_policy_acknowledged is not None,
@ -581,9 +582,64 @@ class OtherContacts(DomainRequestWizard):
return all_forms_valid
class AnythingElse(DomainRequestWizard):
template_name = "domain_request_anything_else.html"
forms = [forms.AnythingElseForm]
class AdditionalDetails(DomainRequestWizard):
template_name = "domain_request_additional_details.html"
forms = [
forms.CisaRepresentativeYesNoForm,
forms.CisaRepresentativeForm,
forms.AdditionalDetailsYesNoForm,
forms.AdditionalDetailsForm,
]
def is_valid(self, forms: list) -> bool:
# Validate Cisa Representative
"""Overrides default behavior defined in DomainRequestWizard.
Depending on value in yes_no forms, marks corresponding data
for deletion. Then validates all forms.
"""
cisa_representative_email_yes_no_form = forms[0]
cisa_representative_email_form = forms[1]
anything_else_yes_no_form = forms[2]
anything_else_form = forms[3]
# ------- Validate cisa representative -------
cisa_rep_portion_is_valid = True
# test first for yes_no_form validity
if cisa_representative_email_yes_no_form.is_valid():
# test for existing data
if not cisa_representative_email_yes_no_form.cleaned_data.get("has_cisa_representative"):
# mark the cisa_representative_email_form for deletion
cisa_representative_email_form.mark_form_for_deletion()
else:
cisa_rep_portion_is_valid = cisa_representative_email_form.is_valid()
else:
# if yes no form is invalid, no choice has been made
# mark the cisa_representative_email_form for deletion
cisa_representative_email_form.mark_form_for_deletion()
cisa_rep_portion_is_valid = False
# ------- Validate anything else -------
anything_else_portion_is_valid = True
# test first for yes_no_form validity
if anything_else_yes_no_form.is_valid():
# test for existing data
if not anything_else_yes_no_form.cleaned_data.get("has_anything_else_text"):
# mark the anything_else_form for deletion
anything_else_form.mark_form_for_deletion()
else:
anything_else_portion_is_valid = anything_else_form.is_valid()
else:
# if yes no form is invalid, no choice has been made
# mark the anything_else_form for deletion
anything_else_form.mark_form_for_deletion()
anything_else_portion_is_valid = False
# ------- Return combined validation result -------
all_forms_valid = cisa_rep_portion_is_valid and anything_else_portion_is_valid
return all_forms_valid
class Requirements(DomainRequestWizard):

View file

@ -1,8 +1,8 @@
-i https://pypi.python.org/simple
annotated-types==0.6.0; python_version >= '3.8'
asgiref==3.8.1; python_version >= '3.8'
boto3==1.34.71; python_version >= '3.8'
botocore==1.34.71; python_version >= '3.8'
boto3==1.34.88; python_version >= '3.8'
botocore==1.34.88; python_version >= '3.8'
cachetools==5.3.3; python_version >= '3.7'
certifi==2024.2.2; python_version >= '3.6'
cfenv==0.5.3
@ -24,28 +24,28 @@ django-login-required-middleware==0.9.0
django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8'
django-widget-tweaks==1.5.0; python_version >= '3.8'
environs[django]==11.0.0; python_version >= '3.8'
faker==24.4.0; python_version >= '3.8'
faker==24.11.0; python_version >= '3.8'
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
furl==2.1.3
future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
gevent==24.2.1; python_version >= '3.8'
greenlet==3.0.3; python_version >= '3.7'
gunicorn==21.2.0; python_version >= '3.5'
idna==3.6; python_version >= '3.5'
gunicorn==22.0.0; python_version >= '3.7'
idna==3.7; python_version >= '3.5'
jmespath==1.0.1; python_version >= '3.7'
lxml==5.1.0; python_version >= '3.6'
mako==1.3.2; python_version >= '3.8'
lxml==5.2.1; python_version >= '3.6'
mako==1.3.3; python_version >= '3.8'
markupsafe==2.1.5; python_version >= '3.7'
marshmallow==3.21.1; python_version >= '3.8'
oic==1.6.1; python_version ~= '3.7'
orderedmultidict==1.0.1
packaging==24.0; python_version >= '3.7'
phonenumberslite==8.13.33
phonenumberslite==8.13.35
psycopg2-binary==2.9.9; python_version >= '3.7'
pycparser==2.21
pycparser==2.22; python_version >= '3.8'
pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pydantic==2.6.4; python_version >= '3.8'
pydantic-core==2.16.3; python_version >= '3.8'
pydantic==2.7.0; python_version >= '3.8'
pydantic-core==2.18.1; python_version >= '3.8'
pydantic-settings==2.2.1; python_version >= '3.8'
pyjwkest==1.4.2
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
@ -53,12 +53,12 @@ python-dotenv==1.0.1; python_version >= '3.8'
pyzipper==0.3.6; python_version >= '3.4'
requests==2.31.0; python_version >= '3.7'
s3transfer==0.10.1; python_version >= '3.8'
setuptools==69.2.0; python_version >= '3.8'
setuptools==69.5.1; 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'
sqlparse==0.5.0; python_version >= '3.8'
tblib==3.0.0; python_version >= '3.8'
typing-extensions==4.10.0; python_version >= '3.8'
typing-extensions==4.11.0; python_version >= '3.8'
urllib3==2.2.1; python_version >= '3.8'
whitenoise==6.6.0; python_version >= '3.8'
zope.event==5.0; python_version >= '3.7'
zope.interface==6.2; python_version >= '3.7'
zope.interface==6.3; python_version >= '3.7'