diff --git a/.github/ISSUE_TEMPLATE/design-issue.yml b/.github/ISSUE_TEMPLATE/design-issue.yml new file mode 100644 index 000000000..558c34c2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/design-issue.yml @@ -0,0 +1,76 @@ +name: Design Issue / story +description: Specifically for design and content tickets +labels: design, content + +body: + - type: markdown + id: title-help + attributes: + value: | + > Titles should be short, descriptive, and compelling. Use sentence case: don't capitalize words unnecessarily. + - type: textarea + id: issue-description + attributes: + label: Issue description + description: | + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. For stories, use the user story format (e.g., As a user, I want, so that). Use full sentences, plain language, and [good formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). + validations: + required: true + - type: markdown + id: acceptance-criteria-design + attributes: + value: | + ### Acceptance criteria for design updates + - [ ] Used components from the Figma design system wherein possible + - [ ] Merged draft design(s) into the official [registrar](https://www.figma.com/design/xFtGLHVrhp0lvwh0gYWnbx/.gov-registar?m=auto) or [get.gov](https://www.figma.com/design/qeWM03sfjXgHBHB23rsD6X/get.gov?m=auto&t=NevyFHpikXLWEwaL-6) Figma pages + - [ ] (If applicable) Updated components from the [Figma design system](https://www.figma.com/design/G2HANRHy8pnlY5ENvmpKY8/.gov-Design-System?m=auto) if there's any inconsistencies with production +
+ - type: markdown + id: acceptance-criteria-content + attributes: + value: | + ### Acceptance criteria for content updates + - [ ] **Followed the [content guide](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?usp=sharing) instructions, including:** + - [ ] Use official terms in the [word list](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?tab=t.0#heading=h.2et92p0) wherein possible, and avoiding synonyms or alternative terms + - [ ] Use [content blocks](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?tab=t.0#heading=h.23ckvvd) whenever possible (consider creating new content blocks for new content that will be referenced heavily) + - [ ] Check for readibility using the [Hemingway Editor](https://hemingwayapp.com/) + - [ ] Any external links have an [external link icon](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?tab=t.0#heading=h.4i7ojhp) and open in a new tab + + - [ ] **Instructions specific to get.gov ([instructions for how to update the public site](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?tab=t.0#heading=h.1pxezwc)):** + - [ ] Update content in the [get.gov design file](https://www.figma.com/design/qeWM03sfjXgHBHB23rsD6X/get.gov?m=auto&t=NevyFHpikXLWEwaL-6) in Figma + - [ ] Update the [relevant pages](https://drive.google.com/drive/u/1/folders/1kPsaM6wTli5yjTx1k1QZNlSzYZ-usW4x) in Google Drive's Content folder + - [ ] Links [open in a new window](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?tab=t.0#heading=h.g0jcbi63s6zm) if the user will need to reference text on the public site while viewing the link + + - [ ] **Instructions specific to the manage.get.gov ([instructions for how to update the registrar](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?tab=t.0#heading=h.1hmsyys)):** + - [ ] Update content in the [registrar design file](https://www.figma.com/design/xFtGLHVrhp0lvwh0gYWnbx/.gov-registar?m=auto) in Figma + - [ ] Update the [relevant pages](https://drive.google.com/drive/folders/1dlv_w9zT-W_TStG-7icag6lqcQi86WUs?usp=drive_link) in Google Drive's Content folder + - [ ] Links [open in a new window](https://docs.google.com/document/d/1U-TRx3ecCFZ-qtrCk7EYmF_nmfKh6kd7FV95Pa4iWeM/edit?tab=t.0#heading=h.g0jcbi63s6zm) + + - [ ] **Instructions specific to emails:** + - [ ] TBD +
+ - type: textarea + id: additional-acceptance-criteria + attributes: + label: Additional acceptance criteria + description: "If known, add more statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." + placeholder: "- [ ]" + - type: textarea + id: additional-context + attributes: + label: Additional context + description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." + - type: textarea + id: links-to-other-issues + attributes: + label: Links to other issues + description: | + "Use a dash (`-`) to start the line. Add an issue by typing "`#`" then the issue number. Add information to describe any dependancies, blockers, etc. (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, πŸ”„ [arrows_counterclockwise] Relates to). If this is a parent issue, use sub-issues instead of linking other issues here." + placeholder: "- πŸ”„ Relates to..." + - type: markdown + id: note + attributes: + value: | + > We may edit the text in this issue to document our understanding and clarify the product work. + + diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 292d0ebec..f9a2f6260 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -19,7 +19,7 @@ body: id: acceptance-criteria attributes: label: Acceptance criteria - description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." + description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate. Designers: [Additional considerations](https://github.com/cisagov/manage.get.gov/wiki/Design-work-considerations) are listed in the wiki and can be adapted to ACs here." placeholder: "- [ ]" - type: textarea id: additional-context diff --git a/docs/developer/README.md b/docs/developer/README.md index 46194bd70..464626b87 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -207,6 +207,17 @@ Linters: docker-compose exec app ./manage.py lint ``` +### Get availability for domain requests to work locally + +If you're on local (localhost:8080) and want to submit a domain request, and keep getting the "We’re experiencing a system error. Please wait a few minutes and try again. If you continue to get this error, contact help@get.gov." error, you can get past the availability check by updating the available() function in registrar/models/domain.py to return True and comment everything else out - see below for reference! + +``` +@classmethod +def available(cls, domain: str) -> bool: + # Comment everything else out in the function + return True +``` + ### Testing behind logged in pages To test behind logged in pages with external tools, like `pa11y-ci` or `OWASP Zap`, add @@ -305,15 +316,15 @@ You can also compile the **Sass** at any time using `npx gulp compile`. Similarl We use the [CSS Block Element Modifier (BEM)](https://getbem.com/naming/) naming convention for our custom classes. This is in line with how USWDS [approaches](https://designsystem.digital.gov/whats-new/updates/2019/04/08/introducing-uswds-2-0/) their CSS class architecture and helps keep our code cohesive and readable. -### Upgrading USWDS and other JavaScript packages +### Updating USWDS 1. Version numbers can be manually controlled in `package.json`. Edit that, if desired. -2. Now run `docker-compose run node npm update`. -3. Then run `docker-compose up` to recompile and recopy the assets, or run `docker-compose updateUswds` if your docker is already up. -4. Make note of the dotgov changes in uswds-edited.js. -5. Copy over the newly compiled code from uswds.js into uswds-edited.js. -6. Put back the dotgov changes you made note of into uswds-edited.js. -7. Examine the results in the running application (remember to empty your cache!) and commit `package.json` and `package-lock.json` if all is well. +2. Now run `npx gulp updateUswds`. Refer to [official docs](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) to see what this is doing. +3. Make note of the dotgov changes in uswds-edited.js (Ctrl-F DOTGOV for modifications to USWDS compiled code). +4. Copy over the newly compiled code from uswds.js into uswds-edited.js. +5. Put back the dotgov changes you made note of into uswds-edited.js. +6. Examine the results in the running application (remember to empty your cache!) and commit `package.json` and `package-lock.json` if all is well. +7. Read the [release notes](https://github.com/uswds/uswds/releases) for the new versions installed, note 'Breaking' and 'Markup change' and make adjustments to the code base as needed. ## Finite State Machines diff --git a/src/Pipfile b/src/Pipfile index 07b1db715..819481fa7 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -django = "4.2.17" +django = "4.2.20" cfenv = "*" django-cors-headers = "*" pycryptodomex = "*" @@ -34,6 +34,7 @@ tblib = "*" django-admin-multiple-choice-list-filter = "*" django-import-export = "*" django-waffle = "*" +cryptography = "*" [dev-packages] django-debug-toolbar = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 76f2c914d..914a217d0 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "07f7bc9bda4099f96b18f8f063b487b121b82ae01de06a7f2e9013d56098a421" + "sha256": "c854531923af84e93b0b26e64a0bf3b9d9c12870c4795b1afb667569ea740e2b" }, "pipfile-spec": 6, "requires": {}, @@ -32,37 +32,37 @@ }, "boto3": { "hashes": [ - "sha256:ba391982f6cada136c5bba99e85d7fe1bc4e157c53a22a78e4aca35d1b39152e", - "sha256:eecef248f8743ab30036cd9c916808a0892fc9036e1a35434d8222060c08bbd2" + "sha256:1545c943f36db41853cdfdb6ff09c4eda9220dd95bd2fae76fc73091603525d1", + "sha256:9b272268794172b0b8bb9fb1f3c470c3b6c0ffb92fbd4882465cc740e40fbdcd" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.91" + "version": "==1.37.18" }, "botocore": { "hashes": [ - "sha256:7b0b9c5954701fff4d2c516918f45641b04ff4ca92bbd9f5b37c0b80f8c14220", - "sha256:93de9d0f52f7e36a2c190d55520d3b2654f32c5a628fdd484bffa00bc7865e1d" + "sha256:99e8eefd5df6347ead15df07ce55f4e62a51ea7b54de1127522a08597923b726", + "sha256:a8b97d217d82b3c4f6bcc906e264df7ebb51e2c6a62b3548a97cd173fb8759a1" ], "markers": "python_version >= '3.8'", - "version": "==1.35.91" + "version": "==1.37.18" }, "cachetools": { "hashes": [ - "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", - "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a" + "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", + "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==5.5.0" + "version": "==5.5.2" }, "certifi": { "hashes": [ - "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", - "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==2024.12.14" + "version": "==2025.1.31" }, "cfenv": { "hashes": [ @@ -245,36 +245,45 @@ }, "cryptography": { "hashes": [ - "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", - "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", - "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", - "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", - "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", - "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", - "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", - "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", - "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", - "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", - "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", - "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", - "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", - "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", - "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", - "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", - "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", - "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", - "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", - "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", - "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", - "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", - "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", - "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", - "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", - "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", - "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4" + "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", + "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", + "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", + "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", + "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", + "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", + "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", + "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", + "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", + "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", + "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", + "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", + "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", + "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", + "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", + "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", + "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", + "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", + "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", + "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", + "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", + "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", + "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", + "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", + "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", + "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", + "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", + "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", + "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", + "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", + "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", + "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", + "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", + "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", + "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308" ], + "index": "pypi", "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==44.0.0" + "version": "==44.0.2" }, "defusedxml": { "hashes": [ @@ -308,12 +317,12 @@ }, "django": { "hashes": [ - "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", - "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc" + "sha256:213381b6e4405f5c8703fffc29cd719efdf189dec60c67c04f76272b3dc845b9", + "sha256:92bac5b4432a64532abb73b2ac27203f485e40225d2640a7fbef2b62b876e789" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.17" + "version": "==4.2.20" }, "django-admin-multiple-choice-list-filter": { "hashes": [ @@ -349,12 +358,12 @@ }, "django-cors-headers": { "hashes": [ - "sha256:14d76b4b4c8d39375baeddd89e4f08899051eeaf177cb02a29bd6eae8cf63aa8", - "sha256:8edbc0497e611c24d5150e0055d3b178c6534b8ed826fb6f53b21c63f5d48ba3" + "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b", + "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.6.0" + "version": "==4.7.0" }, "django-csp": { "hashes": [ @@ -374,12 +383,12 @@ }, "django-import-export": { "hashes": [ - "sha256:91b47c9a2701a5b039667df5c46ee682a41bb224ac215a0e66b177a459e35983", - "sha256:b261f44aedf572a69f975655afba15bff1e354eddd91d9c1bbd32d3cee44168d" + "sha256:5514d09636e84e823a42cd5e79292f70f20d6d2feed117a145f5b64a5b44f168", + "sha256:bd3fe0aa15a2bce9de4be1a2f882e2c4539fdbfdfa16f2052c98dd7aec0f085c" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.3.3" + "version": "==4.3.7" }, "django-login-required-middleware": { "hashes": [ @@ -422,31 +431,31 @@ "django" ], "hashes": [ - "sha256:9d2080cf25807a26fc0d4301e2d7b62c64fbf547540f21e3a30cc02bc5fbe948", - "sha256:e068ae3174cef52ba4b95ead22e639056a02465f616e62323e04ae08e86a75a4" + "sha256:03db7ee2d50ec697b68814cd175a3a05a7c7954804e4e419ca8b570dc5a835cf", + "sha256:45bc56f1d53bbc59d8dd69bba97377dd88ec28b8229d81cedbd455b21789445b" ], - "markers": "python_version >= '3.8'", - "version": "==11.2.1" + "markers": "python_version >= '3.9'", + "version": "==14.1.1" }, "faker": { "hashes": [ - "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4", - "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d" + "sha256:8955706c56c28099585e9e2b6f814eb0a3a227eb36a2ee3eb9ab577c4764eacc", + "sha256:948bd27706478d3aa0b6f9f58b9f25207098f6ca79852c7b49c44a8ced2bc59b" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==33.1.0" + "markers": "python_version >= '3.9'", + "version": "==37.0.2" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", - "ref": "d56d183f1664f34c40ca9716a3a9a345f0ef561c" + "ref": "9f0fd0e69665001767f15a034c9c0c919dab5cdd" }, "furl": { "hashes": [ - "sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e", - "sha256:9ab425062c4217f9802508e45feb4a83e54324273ac4b202f1850363309666c0" + "sha256:877657501266c929269739fb5f5980534a41abd6bbabcb367c136d1d3b2a6015", + "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8" ], - "version": "==2.1.3" + "version": "==2.1.4" }, "future": { "hashes": [ @@ -608,155 +617,155 @@ }, "lxml": { "hashes": [ - "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e", - "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", - "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", - "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5", - "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70", - "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15", - "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", - "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", - "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22", - "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", - "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22", - "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", - "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", - "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", - "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", - "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", - "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f", - "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", - "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", - "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", - "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875", - "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42", - "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e", - "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6", - "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391", - "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", - "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b", - "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237", - "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", - "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", - "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f", - "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", - "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", - "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", - "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903", - "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", - "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", - "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", - "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", - "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", - "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", - "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", - "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", - "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", - "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", - "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", - "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469", - "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", - "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", - "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", - "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", - "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", - "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94", - "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", - "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", - "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84", - "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", - "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", - "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", - "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", - "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", - "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", - "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", - "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", - "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", - "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", - "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", - "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", - "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", - "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040", - "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", - "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8", - "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", - "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", - "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", - "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", - "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce", - "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", - "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577", - "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", - "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", - "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512", - "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", - "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", - "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", - "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", - "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", - "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e", - "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2", - "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27", - "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1", - "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d", - "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", - "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", - "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920", - "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99", - "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", - "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", - "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", - "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", - "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", - "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", - "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19", - "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", - "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", - "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", - "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", - "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", - "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", - "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", - "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", - "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", - "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", - "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", - "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b", - "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753", - "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", - "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", - "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033", - "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", - "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", - "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab", - "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", - "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", - "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", - "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", - "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11", - "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c", - "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", - "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", - "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", - "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", - "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e", - "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", - "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", - "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", - "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945", - "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8" + "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81", + "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131", + "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9", + "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576", + "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1", + "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c", + "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914", + "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607", + "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8", + "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709", + "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b", + "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c", + "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7", + "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2", + "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c", + "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27", + "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33", + "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8", + "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19", + "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae", + "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295", + "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c", + "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b", + "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70", + "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf", + "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79", + "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84", + "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d", + "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423", + "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394", + "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1", + "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270", + "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2", + "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b", + "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd", + "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e", + "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f", + "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439", + "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61", + "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314", + "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0", + "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2", + "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8", + "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0", + "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc", + "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32", + "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499", + "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde", + "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468", + "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe", + "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de", + "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877", + "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5", + "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629", + "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7", + "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a", + "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5", + "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73", + "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f", + "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6", + "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac", + "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65", + "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519", + "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5", + "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8", + "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d", + "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51", + "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf", + "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20", + "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7", + "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53", + "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e", + "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5", + "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5", + "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9", + "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2", + "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff", + "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a", + "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c", + "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549", + "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be", + "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9", + "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125", + "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac", + "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332", + "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2", + "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce", + "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5", + "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a", + "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23", + "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3", + "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c", + "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414", + "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b", + "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9", + "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98", + "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e", + "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3", + "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406", + "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307", + "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098", + "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9", + "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725", + "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd", + "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e", + "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322", + "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd", + "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675", + "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e", + "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a", + "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1", + "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a", + "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb", + "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe", + "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6", + "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50", + "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca", + "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0", + "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78", + "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367", + "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666", + "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645", + "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a", + "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3", + "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252", + "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79", + "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615", + "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f", + "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af", + "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c", + "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7", + "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407", + "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2", + "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac", + "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9", + "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5", + "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2", + "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2" ], "markers": "python_version >= '3.6'", - "version": "==5.3.0" + "version": "==5.3.1" }, "mako": { "hashes": [ - "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627", - "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8" + "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", + "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" ], "markers": "python_version >= '3.8'", - "version": "==1.3.8" + "version": "==1.3.9" }, "markupsafe": { "hashes": [ @@ -827,11 +836,11 @@ }, "marshmallow": { "hashes": [ - "sha256:bcaf2d6fd74fb1459f8450e85d994997ad3e70036452cbfa4ab685acb19479b3", - "sha256:c448ac6455ca4d794773f00bae22c2f351d62d739929f761dce5eacb5c468d7f" + "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", + "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6" ], "markers": "python_version >= '3.9'", - "version": "==3.23.2" + "version": "==3.26.1" }, "oic": { "hashes": [ @@ -859,10 +868,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:02da5e78c67b213bae95afd6289f40486c93e302e518769911dfa5e7287ddeee", - "sha256:dfa44a4bae2e46d737ae5301cb96b14cdcbf45063236c74c6ddb08f5fd471b0d" + "sha256:85e9d75b8de1a5eeb716dc99b3937743cbd5265182dc5c1132204aa491ae0724", + "sha256:cda36bc06b5bd7c042d913408f9ca37c5e6c872f851393f15ad1e9a243e5c753" ], - "version": "==8.13.52" + "version": "==9.0.1" }, "psycopg2-binary": { "hashes": [ @@ -878,6 +887,7 @@ "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", + "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", @@ -948,50 +958,47 @@ }, "pycryptodomex": { "hashes": [ - "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3", - "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516", - "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f", - "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c", - "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e", - "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e", - "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c", - "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31", - "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b", - "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832", - "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e", - "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b", - "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37", - "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65", - "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a", - "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3", - "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b", - "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9", - "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971", - "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2", - "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42", - "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd", - "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e", - "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0", - "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c", - "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a", - "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce", - "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6", - "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822", - "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9", - "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00", - "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8" + "sha256:140b27caa68a36d0501b05eb247bd33afa5f854c1ee04140e38af63c750d4e39", + "sha256:1a8b0c5ba061ace4bcd03496d42702c3927003db805b8ec619ea6506080b381d", + "sha256:1ab0d89d1761959b608952c7b347b0e76a32d1a5bb278afbaa10a7f3eaef9a0a", + "sha256:259664c4803a1fa260d5afb322972813c5fe30ea8b43e54b03b7e3a27b30856b", + "sha256:276be1ed006e8fd01bba00d9bd9b60a0151e478033e86ea1cb37447bbc057edc", + "sha256:2cac9ed5c343bb3d0075db6e797e6112514764d08d667c74cb89b931aac9dddd", + "sha256:41673e5cc39a8524557a0472077635d981172182c9fe39ce0b5f5c19381ffaff", + "sha256:5ac608a6dce9418d4f300fab7ba2f7d499a96b462f2b9b5c90d8d994cd36dcad", + "sha256:5bf3ce9211d2a9877b00b8e524593e2209e370a287b3d5e61a8c45f5198487e2", + "sha256:5e64164f816f5e43fd69f8ed98eb28f98157faf68208cd19c44ed9d8e72d33e8", + "sha256:5ebc09b7d8964654aaf8a4f5ac325f2b0cc038af9bea12efff0cd4a5bb19aa42", + "sha256:644834b1836bb8e1d304afaf794d5ae98a1d637bd6e140c9be7dd192b5374811", + "sha256:684cb57812cd243217c3d1e01a720c5844b30f0b7b64bb1a49679f7e1e8a54ac", + "sha256:7127d9de3c7ce20339e06bcd4f16f1a1a77f1471bcf04e3b704306dde101b719", + "sha256:72c506aba3318505dbeecf821ed7b9a9f86f422ed085e2d79c4fba0ae669920a", + "sha256:7a24f681365ec9757ccd69b85868bbd7216ba451d0f86f6ea0eed75eeb6975db", + "sha256:7cd39f7a110c1ab97ce9ee3459b8bc615920344dc00e56d1b709628965fba3f2", + "sha256:813e57da5ceb4b549bab96fa548781d9a63f49f1d68fdb148eeac846238056b7", + "sha256:a1da61bacc22f93a91cbe690e3eb2022a03ab4123690ab16c46abb693a9df63d", + "sha256:aef4590263b9f2f6283469e998574d0bd45c14fb262241c27055b82727426157", + "sha256:b3746dedf74787da43e4a2f85bd78f5ec14d2469eb299ddce22518b3891f16ea", + "sha256:bfe4fe3233ef3e58028a3ad8f28473653b78c6d56e088ea04fe7550c63d4d16b", + "sha256:c8cffb03f5dee1026e3f892f7cffd79926a538c67c34f8b07c90c0bd5c834e27", + "sha256:d7beeacb5394765aa8dabed135389a11ee322d3ee16160d178adc7f8ee3e1f65", + "sha256:e4eaaf6163ff13788c1f8f615ad60cdc69efac6d3bf7b310b21e8cfe5f46c801", + "sha256:eac39e237d65981554c2d4c6668192dc7051ad61ab5fc383ed0ba049e4007ca2", + "sha256:ee75067b35c93cc18b38af47b7c0664998d8815174cfc66dd00ea1e244eb27e6", + "sha256:f005de31efad6f9acefc417296c641f13b720be7dbfec90edeaca601c0fab048", + "sha256:ff46212fda7ee86ec2f4a64016c994e8ad80f11ef748131753adb67e9b722ebd" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==3.21.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==3.22.0" }, "pydantic": { "hashes": [ - "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d", - "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06" + "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", + "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236" ], "markers": "python_version >= '3.8'", - "version": "==2.10.4" + "version": "==2.10.6" }, "pydantic-core": { "hashes": [ @@ -1101,11 +1108,11 @@ }, "pydantic-settings": { "hashes": [ - "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", - "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd" + "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", + "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585" ], "markers": "python_version >= '3.8'", - "version": "==2.7.1" + "version": "==2.8.1" }, "pyjwkest": { "hashes": [ @@ -1150,19 +1157,19 @@ }, "s3transfer": { "hashes": [ - "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", - "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7" + "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679", + "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d" ], "markers": "python_version >= '3.8'", - "version": "==0.10.4" + "version": "==0.11.4" }, "setuptools": { "hashes": [ - "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", - "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d" + "sha256:583b361c8da8de57403743e756609670de6fb2345920e36dc5c2d914c319c945", + "sha256:67122e78221da5cf550ddd04cf8742c8fe12094483749a792d56cd669d6cf58c" ], "markers": "python_version >= '3.9'", - "version": "==75.6.0" + "version": "==77.0.3" }, "six": { "hashes": [ @@ -1182,11 +1189,11 @@ }, "tablib": { "hashes": [ - "sha256:9a6930037cfe0f782377963ca3f2b1dae3fd4cdbf0883848f22f1447e7bb718b", - "sha256:f9db84ed398df5109bd69c11d46613d16cc572fb9ad3213f10d95e2b5f12c18e" + "sha256:35bdb9d4ec7052232f8803908f9c7a9c3c65807188b70618fa7a7d8ccd560b4d", + "sha256:94d8bcdc65a715a0024a6d5b701a5f31e45bd159269e62c73731de79f048db2b" ], "markers": "python_version >= '3.9'", - "version": "==3.7.0" + "version": "==3.8.0" }, "tblib": { "hashes": [ @@ -1206,6 +1213,14 @@ "markers": "python_version >= '3.8'", "version": "==4.12.2" }, + "tzdata": { + "hashes": [ + "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", + "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639" + ], + "markers": "python_version >= '2'", + "version": "==2025.1" + }, "urllib3": { "hashes": [ "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", @@ -1216,12 +1231,12 @@ }, "whitenoise": { "hashes": [ - "sha256:486bd7267a375fa9650b136daaec156ac572971acc8bf99add90817a530dd1d4", - "sha256:df12dce147a043d1956d81d288c6f0044147c6d2ab9726e5772ac50fb45d2280" + "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609", + "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==6.8.2" + "version": "==6.9.0" }, "zope.event": { "hashes": [ @@ -1286,49 +1301,49 @@ }, "bandit": { "hashes": [ - "sha256:b1a61d829c0968aed625381e426aa378904b996529d048f8d908fa28f6b13e38", - "sha256:b5bfe55a095abd9fe20099178a7c6c060f844bfd4fe4c76d28e35e4c52b9d31e" + "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8", + "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.8.0" + "version": "==1.8.3" }, "beautifulsoup4": { "hashes": [ - "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", - "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" + "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", + "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16" ], - "markers": "python_full_version >= '3.6.0'", - "version": "==4.12.3" + "markers": "python_full_version >= '3.7.0'", + "version": "==4.13.3" }, "black": { "hashes": [ - "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", - "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", - "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", - "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", - "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", - "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", - "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", - "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", - "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", - "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", - "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", - "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", - "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", - "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", - "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", - "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", - "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", - "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", - "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", - "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", - "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", - "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e" + "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", + "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", + "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", + "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", + "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", + "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", + "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", + "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", + "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", + "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", + "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", + "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", + "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", + "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", + "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", + "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", + "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", + "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", + "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", + "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", + "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", + "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==24.10.0" + "version": "==25.1.0" }, "blinker": { "hashes": [ @@ -1340,12 +1355,12 @@ }, "boto3": { "hashes": [ - "sha256:ba391982f6cada136c5bba99e85d7fe1bc4e157c53a22a78e4aca35d1b39152e", - "sha256:eecef248f8743ab30036cd9c916808a0892fc9036e1a35434d8222060c08bbd2" + "sha256:1545c943f36db41853cdfdb6ff09c4eda9220dd95bd2fae76fc73091603525d1", + "sha256:9b272268794172b0b8bb9fb1f3c470c3b6c0ffb92fbd4882465cc740e40fbdcd" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.91" + "version": "==1.37.18" }, "boto3-mocking": { "hashes": [ @@ -1358,28 +1373,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:780f71406147b78f9860d78907b5c015874537d821364588ec837c4cd1eecf91", - "sha256:e4301b9d05b31fbfea382d0d1d950c2178f7fca03058b31373fac9a4cdf89438" + "sha256:30ecf8dfaa848469f55279e41248ff46b1ecf5e2ee7b3b292e3ec39332e2f90e", + "sha256:db866104d3b9bea63c4769255759ad5c61bc5e5591cea0b692df4c08082a841e" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.91" + "version": "==1.37.18" }, "botocore": { "hashes": [ - "sha256:7b0b9c5954701fff4d2c516918f45641b04ff4ca92bbd9f5b37c0b80f8c14220", - "sha256:93de9d0f52f7e36a2c190d55520d3b2654f32c5a628fdd484bffa00bc7865e1d" + "sha256:99e8eefd5df6347ead15df07ce55f4e62a51ea7b54de1127522a08597923b726", + "sha256:a8b97d217d82b3c4f6bcc906e264df7ebb51e2c6a62b3548a97cd173fb8759a1" ], "markers": "python_version >= '3.8'", - "version": "==1.35.91" + "version": "==1.37.18" }, "botocore-stubs": { "hashes": [ - "sha256:c6b294cae436eaaf87dcb717e4348c250ea1fc170336579da114b693663d8e42", - "sha256:f7fd78d84f49d28692662b9bdeb4c92f1bf8a5707d0c28c8544399005b02823b" + "sha256:b9c3a1e8fb57fb70b49aa5380cabefab32ec028d8a1d8f5ac83dd836c5b429a8", + "sha256:c6cb18979a86db311a365448b67e4a492a530c3f4fb313432d41deaee6268b95" ], "markers": "python_version >= '3.8'", - "version": "==1.35.90" + "version": "==1.37.17" }, "click": { "hashes": [ @@ -1391,21 +1406,21 @@ }, "django": { "hashes": [ - "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", - "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc" + "sha256:213381b6e4405f5c8703fffc29cd719efdf189dec60c67c04f76272b3dc845b9", + "sha256:92bac5b4432a64532abb73b2ac27203f485e40225d2640a7fbef2b62b876e789" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.17" + "version": "==4.2.20" }, "django-debug-toolbar": { "hashes": [ - "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044", - "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45" + "sha256:8a3b9da4aeab8d384a366e20304bd939a451f0242523c5b7b402248ad474eed2", + "sha256:c0591e338ee9603bdfce5aebf8d18ca7341fdbb69595e2b0b34869be5857180e" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.4.6" + "markers": "python_version >= '3.9'", + "version": "==5.1.0" }, "django-model2puml": { "hashes": [ @@ -1416,20 +1431,20 @@ }, "django-stubs": { "hashes": [ - "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b", - "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac" + "sha256:716758ced158b439213062e52de6df3cff7c586f9f9ad7ab59210efbea5dfe78", + "sha256:8c230bc5bebee6da282ba8a27ad1503c84a0c4cd2f46e63d149e76d2a63e639a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.1" + "version": "==5.1.3" }, "django-stubs-ext": { "hashes": [ - "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c", - "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c" + "sha256:3e60f82337f0d40a362f349bf15539144b96e4ceb4dbd0239be1cd71f6a74ad0", + "sha256:64561fbc53e963cc1eed2c8eb27e18b8e48dcb90771205180fe29fc8a59e55fd" ], "markers": "python_version >= '3.8'", - "version": "==5.1.1" + "version": "==5.1.3" }, "django-webtest": { "hashes": [ @@ -1441,12 +1456,12 @@ }, "flake8": { "hashes": [ - "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", - "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213" + "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", + "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd" ], "index": "pypi", "markers": "python_full_version >= '3.8.1'", - "version": "==7.1.1" + "version": "==7.1.2" }, "jmespath": { "hashes": [ @@ -1482,48 +1497,42 @@ }, "mypy": { "hashes": [ - "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", - "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", - "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", - "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", - "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", - "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", - "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", - "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", - "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", - "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", - "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", - "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", - "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", - "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", - "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", - "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", - "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", - "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", - "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", - "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", - "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", - "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", - "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", - "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", - "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", - "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", - "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", - "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", - "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", - "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", - "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", - "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", - "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", - "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", - "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", - "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", - "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", - "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89" + "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", + "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", + "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", + "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", + "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", + "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", + "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", + "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", + "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", + "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", + "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", + "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", + "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", + "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", + "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", + "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", + "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", + "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", + "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", + "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", + "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", + "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", + "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", + "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", + "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", + "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", + "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", + "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", + "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", + "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", + "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", + "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.14.1" + "markers": "python_version >= '3.9'", + "version": "==1.15.0" }, "mypy-extensions": { "hashes": [ @@ -1559,19 +1568,19 @@ }, "pbr": { "hashes": [ - "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24", - "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a" + "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", + "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b" ], "markers": "python_version >= '2.6'", - "version": "==6.1.0" + "version": "==6.1.1" }, "platformdirs": { "hashes": [ - "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", - "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" + "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", + "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351" ], - "markers": "python_version >= '3.8'", - "version": "==4.3.6" + "markers": "python_version >= '3.9'", + "version": "==4.3.7" }, "pycodestyle": { "hashes": [ @@ -1591,11 +1600,11 @@ }, "pygments": { "hashes": [ - "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", - "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" + "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", + "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" ], "markers": "python_version >= '3.8'", - "version": "==2.18.0" + "version": "==2.19.1" }, "python-dateutil": { "hashes": [ @@ -1674,11 +1683,19 @@ }, "s3transfer": { "hashes": [ - "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", - "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7" + "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679", + "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d" ], "markers": "python_version >= '3.8'", - "version": "==0.10.4" + "version": "==0.11.4" + }, + "setuptools": { + "hashes": [ + "sha256:583b361c8da8de57403743e756609670de6fb2345920e36dc5c2d914c319c945", + "sha256:67122e78221da5cf550ddd04cf8742c8fe12094483749a792d56cd669d6cf58c" + ], + "markers": "python_version >= '3.9'", + "version": "==77.0.3" }, "six": { "hashes": [ @@ -1706,11 +1723,11 @@ }, "stevedore": { "hashes": [ - "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d", - "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857" + "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", + "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe" ], "markers": "python_version >= '3.9'", - "version": "==5.4.0" + "version": "==5.4.1" }, "tomli": { "hashes": [ @@ -1752,11 +1769,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:405bce8c281f9e7c6c92a229225cc0bf10d30729a6a601123213389bd524b8b1", - "sha256:fbf9c221af5607b24bf17f8431217ce8b9a27917139edbc984891eb63fd5a593" + "sha256:345ab84a4f75b26bfb816b249657855824a4f2d1ce5b58268c549f81fce6eccc", + "sha256:5826baf69ad5d29c76be49fc7df00222281fa31b14f99e9fb4492d71ec98fea5" ], "markers": "python_version >= '3.8'", - "version": "==0.23.6" + "version": "==0.24.2" }, "types-cachetools": { "hashes": [ @@ -1777,20 +1794,20 @@ }, "types-requests": { "hashes": [ - "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", - "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747" + "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1", + "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.32.0.20241016" + "markers": "python_version >= '3.9'", + "version": "==2.32.0.20250306" }, "types-s3transfer": { "hashes": [ - "sha256:03123477e3064c81efe712bf9d372c7c72f2790711431f9baa59cf96ea607267", - "sha256:22ac1aabc98f9d7f2928eb3fb4d5c02bf7435687f0913345a97dd3b84d0c217d" + "sha256:05fde593c84270f19fd053f0b1e08f5a057d7c5f036b9884e68fb8cd3041ac30", + "sha256:2a76d92c07d4a3cb469e5343b2e7560e0b8078b2e03696a65407b8c44c861b61" ], "markers": "python_version >= '3.8'", - "version": "==0.10.4" + "version": "==0.11.4" }, "typing-extensions": { "hashes": [ @@ -1827,11 +1844,11 @@ }, "webtest": { "hashes": [ - "sha256:0b2de681c16f57b31da5cce6e94ff03cdc77bd86c37a57ba0ee27fed8e065ceb", - "sha256:799846e169d15e0c1233ab4ab00ee4de59a5d964407d6f2945d89249328dbbdb" + "sha256:5b3d8c69ac9057f17750ed5b45320a411423c2b4196bec6450961be98b03d8c1", + "sha256:94778d19a37e5abd7388dad4d93874410ecced53a1739a8e5ff2dbcba1cfc0c4" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.2" + "markers": "python_version >= '3.9'", + "version": "==3.0.4" } } } diff --git a/src/package-lock.json b/src/package-lock.json index 0f2a8c38b..f46ecd3e8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -63,23 +63,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz", - "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.8", + "@babel/generator": "^7.26.9", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.7", - "@babel/parser": "^7.26.8", - "@babel/template": "^7.26.8", - "@babel/traverse": "^7.26.8", - "@babel/types": "^7.26.8", - "@types/gensync": "^1.0.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -95,14 +94,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz", - "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.8", - "@babel/types": "^7.26.8", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -142,18 +141,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", + "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/traverse": "^7.26.9", "semver": "^6.3.1" }, "engines": { @@ -363,27 +362,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", - "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.8" + "@babel/types": "^7.26.9" }, "bin": { "parser": "bin/babel-parser.js" @@ -809,13 +808,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", + "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { @@ -1376,9 +1375,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.8.tgz", - "integrity": "sha512-um7Sy+2THd697S4zJEfv/U5MHGJzkN2xhtsR3T/SWRbVSic62nbISh51VVfU9JiO/L/Z97QczHTaFVkOU8IzNg==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1411,7 +1410,7 @@ "@babel/plugin-transform-dynamic-import": "^7.25.9", "@babel/plugin-transform-exponentiation-operator": "^7.26.3", "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", "@babel/plugin-transform-function-name": "^7.25.9", "@babel/plugin-transform-json-strings": "^7.25.9", "@babel/plugin-transform-literals": "^7.25.9", @@ -1475,9 +1474,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", - "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", "dev": true, "license": "MIT", "dependencies": { @@ -1488,32 +1487,32 @@ } }, "node_modules/@babel/template": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", - "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.8", - "@babel/types": "^7.26.8" + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz", - "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.8", - "@babel/parser": "^7.26.8", - "@babel/template": "^7.26.8", - "@babel/types": "^7.26.8", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1522,9 +1521,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", - "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", "dev": true, "license": "MIT", "dependencies": { @@ -1999,13 +1998,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/gensync": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz", - "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2014,9 +2006,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2255,9 +2247,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -2868,9 +2860,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001699", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", - "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", + "version": "1.0.30001702", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz", + "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==", "dev": true, "funding": [ { @@ -3142,13 +3134,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", - "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.3" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -3371,9 +3363,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.97", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz", - "integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==", + "version": "1.5.113", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.113.tgz", + "integrity": "sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==", "dev": true, "license": "ISC" }, @@ -3661,13 +3653,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-levenshtein": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", @@ -3706,9 +3691,9 @@ } }, "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -5718,16 +5703,6 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/puppeteer": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz", @@ -6113,9 +6088,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -6164,9 +6139,9 @@ } }, "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6186,9 +6161,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.84.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz", - "integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==", + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -6661,9 +6636,9 @@ } }, "node_modules/sass/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -6978,9 +6953,9 @@ } }, "node_modules/terser": { - "version": "5.38.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.2.tgz", - "integrity": "sha512-w8CXxxbFA5zfNsR/i8HZq5bvn18AK0O9jj7hyo1YqkovLxEFa0uP0LCVGZRqiRaKRFxXhELBp8SteeAjEnfeJg==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6997,9 +6972,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", - "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7229,9 +7204,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -7259,16 +7234,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7522,9 +7487,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", "dependencies": { @@ -7546,9 +7511,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -7617,59 +7582,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -7842,9 +7754,9 @@ } }, "node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.0.tgz", + "integrity": "sha512-KHBC7z61OJeaMGnF3wqNZj+GGNXOyypZviiKpQeiHirG5Ib1ImwcLBH70rbMSkKfSmUNBsdf2PwaEJtKvgmkNw==", "dev": true, "license": "MIT", "engines": { diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 50f04a0fe..b625ef3c3 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -76,6 +76,19 @@ from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) +class ImportExportRegistrarModelAdmin(ImportExportModelAdmin): + + def has_import_permission(self, request): + return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm( + "registrar.full_access_permission" + ) + + def has_export_permission(self, request): + return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm( + "registrar.full_access_permission" + ) + + class FsmModelResource(resources.ModelResource): """ModelResource is extended to support importing of tables which have FSMFields. ModelResource is extended with the following changes @@ -466,7 +479,7 @@ class DomainRequestAdminForm(forms.ModelForm): # only set the available transitions if the user is not restricted # from editing the domain request; otherwise, the form will be # readonly and the status field will not have a widget - if not domain_request.creator.is_restricted(): + if not domain_request.creator.is_restricted() and "status" in self.fields: self.fields["status"].widget.choices = available_transitions def get_custom_field_transitions(self, instance, field): @@ -920,7 +933,7 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin): return filters -class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): +class MyUserAdmin(BaseUserAdmin, ImportExportRegistrarModelAdmin): """Custom user admin class to use our inlines.""" resource_classes = [UserResource] @@ -1225,7 +1238,7 @@ class HostResource(resources.ModelResource): model = models.Host -class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin): +class MyHostAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin): """Custom host admin class to use our inlines.""" resource_classes = [HostResource] @@ -1243,7 +1256,7 @@ class HostIpResource(resources.ModelResource): model = models.HostIP -class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin): +class HostIpAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin): """Custom host ip admin class""" resource_classes = [HostIpResource] @@ -1258,7 +1271,7 @@ class ContactResource(resources.ModelResource): model = models.Contact -class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class ContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Custom contact admin class to add search.""" resource_classes = [ContactResource] @@ -1392,6 +1405,59 @@ class SeniorOfficialAdmin(ListHeaderAdmin): # in autocomplete_fields for Senior Official ordering = ["first_name", "last_name"] + readonly_fields = [] + + # Even though this is empty, I will leave it as a stub for easy changes in the future + # rather than strip it out of our logic. + analyst_readonly_fields = [] # type: ignore + + omb_analyst_readonly_fields = [ + "first_name", + "last_name", + "title", + "phone", + "email", + "federal_agency", + ] + + def get_readonly_fields(self, request, obj=None): + """Set the read-only state on form elements. + We have conditions that determine which fields are read-only: + admin user permissions and analyst (cisa or omb) status, so + we'll use the baseline readonly_fields and extend it as needed. + """ + readonly_fields = list(self.readonly_fields) + + if request.user.has_perm("registrar.full_access_permission"): + return readonly_fields + # Return restrictive Read-only fields for OMB analysts + if request.user.groups.filter(name="omb_analysts_group").exists(): + readonly_fields.extend([field for field in self.omb_analyst_readonly_fields]) + return readonly_fields + # Return restrictive Read-only fields for analysts and + # users who might not belong to groups + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields + + def get_queryset(self, request): + """Restrict queryset based on user permissions.""" + qs = super().get_queryset(request) + + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE) + + return qs # Return full queryset if the user doesn't have the restriction + + def has_view_permission(self, request, obj=None): + """Restrict view permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return obj.federal_agency and obj.federal_agency.federal_type == BranchChoices.EXECUTIVE + return super().has_view_permission(request, obj) + class WebsiteResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -1401,7 +1467,7 @@ class WebsiteResource(resources.ModelResource): model = models.Website -class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class WebsiteAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Custom website admin class.""" resource_classes = [WebsiteResource] @@ -1502,7 +1568,7 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin): obj.delete() # Calls the overridden delete method on each instance -class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Custom user domain role admin class.""" resource_classes = [UserDomainRoleResource] @@ -1685,6 +1751,63 @@ class DomainInvitationAdmin(BaseInvitationAdmin): # Override for the delete confirmation page on the domain table (bulk delete action) delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html" + def get_annotated_queryset(self, queryset): + return queryset.annotate( + converted_generic_org_type=Case( + # When portfolio is present, use its value instead + When( + domain__domain_info__portfolio__isnull=False, + then=F("domain__domain_info__portfolio__organization_type"), + ), + # Otherwise, return the natively assigned value + default=F("domain__domain_info__generic_org_type"), + ), + converted_federal_type=Case( + # When portfolio is present, use its value instead + When( + Q(domain__domain_info__portfolio__isnull=False) + & Q(domain__domain_info__portfolio__federal_agency__isnull=False), + then=F("domain__domain_info__portfolio__federal_agency__federal_type"), + ), + # Otherwise, return the federal agency's federal_type + default=F("domain__domain_info__federal_agency__federal_type"), + ), + ) + + def get_queryset(self, request): + """Restrict queryset based on user permissions.""" + qs = super().get_queryset(request) + + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + annotated_qs = self.get_annotated_queryset(qs) + return annotated_qs.filter( + converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + converted_federal_type=BranchChoices.EXECUTIVE, + ) + + return qs # Return full queryset if the user doesn't have the restriction + + def has_view_permission(self, request, obj=None): + """Restrict view permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return ( + obj.domain.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL + and obj.domain.domain_info.converted_federal_type == BranchChoices.EXECUTIVE + ) + return super().has_view_permission(request, obj) + + # Select domain invitations to change -> Domain invitations + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Domain invitations" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + def change_view(self, request, object_id, form_url="", extra_context=None): """Override the change_view to add the invitation obj for the change_form_object_tools template""" @@ -1847,7 +1970,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin): requested_user = get_requested_user(requested_email) permission_exists = UserPortfolioPermission.objects.filter( - user__email=requested_email, portfolio=portfolio, user__email__isnull=False + user__email__iexact=requested_email, portfolio=portfolio, user__email__isnull=False ).exists() if not permission_exists: # if permission does not exist for a user with requested_email, send email @@ -1857,9 +1980,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin): portfolio=portfolio, is_admin_invitation=is_admin_invitation, ): - messages.warning( - self.request, "Could not send email notification to existing organization admins." - ) + messages.warning(request, "Could not send email notification to existing organization admins.") # if user exists for email, immediately retrieve portfolio invitation upon creation if requested_user is not None: obj.retrieve() @@ -1908,7 +2029,7 @@ class DomainInformationResource(resources.ModelResource): model = models.DomainInformation -class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class DomainInformationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Customize domain information admin class.""" class GenericOrgFilter(admin.SimpleListFilter): @@ -2185,6 +2306,47 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): "is_policy_acknowledged", ] + # Read only that we'll leverage for OMB Analysts + omb_analyst_readonly_fields = [ + "federal_agency", + "creator", + "about_your_organization", + "anything_else", + "cisa_representative_first_name", + "cisa_representative_last_name", + "cisa_representative_email", + "domain_request", + "notes", + "senior_official", + "organization_type", + "organization_name", + "state_territory", + "address_line1", + "address_line2", + "city", + "zipcode", + "urbanization", + "portfolio_organization_type", + "portfolio_federal_type", + "portfolio_organization_name", + "portfolio_federal_agency", + "portfolio_state_territory", + "portfolio_address_line1", + "portfolio_address_line2", + "portfolio_city", + "portfolio_zipcode", + "portfolio_urbanization", + "organization_type", + "federal_type", + "federal_agency", + "tribe_name", + "federally_recognized_tribe", + "state_recognized_tribe", + "about_your_organization", + "portfolio", + "sub_organization", + ] + # For each filter_horizontal, init in admin js initFilterHorizontalWidget # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) @@ -2213,6 +2375,10 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): if request.user.has_perm("registrar.full_access_permission"): return readonly_fields + # Return restrictive Read-only fields for OMB analysts + if request.user.groups.filter(name="omb_analysts_group").exists(): + readonly_fields.extend([field for field in self.omb_analyst_readonly_fields]) + return readonly_fields # Return restrictive Read-only fields for analysts and # users who might not belong to groups readonly_fields.extend([field for field in self.analyst_readonly_fields]) @@ -2229,6 +2395,38 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): use_sort = db_field.name != "senior_official" return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs) + def get_annotated_queryset(self, queryset): + return queryset.annotate( + conv_generic_org_type=Case( + # When portfolio is present, use its value instead + When(portfolio__isnull=False, then=F("portfolio__organization_type")), + # Otherwise, return the natively assigned value + default=F("generic_org_type"), + ), + conv_federal_type=Case( + # When portfolio is present, use its value instead + When( + Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False), + then=F("portfolio__federal_agency__federal_type"), + ), + # Otherwise, return the federal_type from federal agency + default=F("federal_agency__federal_type"), + ), + ) + + def get_queryset(self, request): + """Custom get_queryset to filter by portfolio if portfolio is in the + request params.""" + qs = super().get_queryset(request) + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + annotated_qs = self.get_annotated_queryset(qs) + return annotated_qs.filter( + conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + conv_federal_type=BranchChoices.EXECUTIVE, + ) + return qs + class DomainRequestResource(FsmModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -2238,7 +2436,7 @@ class DomainRequestResource(FsmModelResource): model = models.DomainRequest -class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Custom domain requests admin class.""" resource_classes = [DomainRequestResource] @@ -2296,7 +2494,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): class FederalTypeFilter(admin.SimpleListFilter): """Custom Federal Type filter that accomodates portfolio feature. If we have a portfolio, use the portfolio's federal type. If not, use the - organization in the Domain Request object.""" + organization in the Domain Request object's federal agency.""" title = "federal type" parameter_name = "converted_federal_types" @@ -2337,7 +2535,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if self.value(): return queryset.filter( Q(portfolio__federal_agency__federal_type=self.value()) - | Q(portfolio__isnull=True, federal_type=self.value()) + | Q(portfolio__isnull=True, federal_agency__federal_type=self.value()) ) return queryset @@ -2752,6 +2950,62 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "cisa_representative_email", ] + # Read only that we'll leverage for OMB Analysts + omb_analyst_readonly_fields = [ + "federal_agency", + "creator", + "about_your_organization", + "requested_domain", + "approved_domain", + "alternative_domains", + "purpose", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + "cisa_representative_first_name", + "cisa_representative_last_name", + "cisa_representative_email", + "status", + "investigator", + "notes", + "senior_official", + "organization_type", + "organization_name", + "state_territory", + "address_line1", + "address_line2", + "city", + "zipcode", + "urbanization", + "portfolio_organization_type", + "portfolio_federal_type", + "portfolio_organization_name", + "portfolio_federal_agency", + "portfolio_state_territory", + "portfolio_address_line1", + "portfolio_address_line2", + "portfolio_city", + "portfolio_zipcode", + "portfolio_urbanization", + "is_election_board", + "organization_type", + "federal_type", + "federal_agency", + "tribe_name", + "federally_recognized_tribe", + "state_recognized_tribe", + "about_your_organization", + "rejection_reason", + "rejection_reason_email", + "action_needed_reason", + "action_needed_reason_email", + "portfolio", + "sub_organization", + "requested_suborganization", + "suborganization_city", + "suborganization_state_territory", + ] + autocomplete_fields = [ "approved_domain", "requested_domain", @@ -3003,6 +3257,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if request.user.has_perm("registrar.full_access_permission"): return readonly_fields + # Return restrictive Read-only fields for OMB analysts + if request.user.groups.filter(name="omb_analysts_group").exists(): + readonly_fields.extend([field for field in self.omb_analyst_readonly_fields]) + return readonly_fields # Return restrictive Read-only fields for analysts and # users who might not belong to groups readonly_fields.extend([field for field in self.analyst_readonly_fields]) @@ -3186,6 +3444,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): use_sort = db_field.name != "senior_official" return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs) + def get_annotated_queryset(self, queryset): + return queryset.annotate( + conv_generic_org_type=Case( + # When portfolio is present, use its value instead + When(portfolio__isnull=False, then=F("portfolio__organization_type")), + # Otherwise, return the natively assigned value + default=F("generic_org_type"), + ), + conv_federal_type=Case( + # When portfolio is present, use its value instead + When( + Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False), + then=F("portfolio__federal_agency__federal_type"), + ), + # Otherwise, return federal type from federal agency + default=F("federal_agency__federal_type"), + ), + ) + def get_queryset(self, request): """Custom get_queryset to filter by portfolio if portfolio is in the request params.""" @@ -3195,8 +3472,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if portfolio_id: # Further filter the queryset by the portfolio qs = qs.filter(portfolio=portfolio_id) + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + annotated_qs = self.get_annotated_queryset(qs) + return annotated_qs.filter( + conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + conv_federal_type=BranchChoices.EXECUTIVE, + ) return qs + def has_view_permission(self, request, obj=None): + """Restrict view permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return ( + obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL + and obj.converted_federal_type == BranchChoices.EXECUTIVE + ) + return super().has_view_permission(request, obj) + + def has_change_permission(self, request, obj=None): + """Restrict update permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return ( + obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL + and obj.converted_federal_type == BranchChoices.EXECUTIVE + ) + return super().has_change_permission(request, obj) + def get_search_results(self, request, queryset, search_term): # Call the parent's method to apply default search logic base_queryset, use_distinct = super().get_search_results(request, queryset, search_term) @@ -3216,6 +3524,15 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return combined_queryset, use_distinct + def get_form(self, request, obj=None, **kwargs): + """Pass the 'is_omb_analyst' attribute to the form.""" + form = super().get_form(request, obj, **kwargs) + + # Store attribute in the form for template access + form.show_contact_as_plain_text = request.user.groups.filter(name="omb_analysts_group").exists() + + return form + class TransitionDomainAdmin(ListHeaderAdmin): """Custom transition domain admin class.""" @@ -3247,6 +3564,16 @@ class DomainInformationInline(admin.StackedInline): template = "django/admin/includes/domain_info_inline_stacked.html" model = models.DomainInformation + def __init__(self, *args, **kwargs): + """Initialize the admin class and define a default value for is_omb_analyst.""" + super().__init__(*args, **kwargs) + self.is_omb_analyst = False # Default value in case it's accessed before being set + + def get_queryset(self, request): + """Ensure self.is_omb_analyst is set early.""" + self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists() + return super().get_queryset(request) + # Define methods to display fields from the related portfolio def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]: return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None @@ -3314,6 +3641,7 @@ class DomainInformationInline(admin.StackedInline): fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets)) readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields) analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields) + omb_analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.omb_analyst_readonly_fields) autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields) def get_domain_managers(self, obj): @@ -3334,12 +3662,16 @@ class DomainInformationInline(admin.StackedInline): if not domain_managers: return "No domain managers found." - domain_manager_details = "" + domain_manager_details = "
UIDNameEmail
" + if not self.is_omb_analyst: + domain_manager_details += "" + domain_manager_details += "" for domain_manager in domain_managers: full_name = domain_manager.get_formatted_name() change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk]) domain_manager_details += "" - domain_manager_details += f'" domain_manager_details += f"" domain_manager_details += "" @@ -3371,7 +3703,8 @@ class DomainInformationInline(admin.StackedInline): superuser_perm = request.user.has_perm("registrar.full_access_permission") analyst_perm = request.user.has_perm("registrar.analyst_access_permission") - if analyst_perm and not superuser_perm: + omb_analyst_perm = request.user.groups.filter(name="omb_analysts_group").exists() + if (analyst_perm or omb_analyst_perm) and not superuser_perm: return True return super().has_change_permission(request, obj) @@ -3445,6 +3778,23 @@ class DomainInformationInline(admin.StackedInline): return modified_fieldsets + def get_form(self, request, obj=None, **kwargs): + """Pass the 'is_omb_analyst' attribute to the form.""" + form = super().get_form(request, obj, **kwargs) + + # Store attribute in the form for template access + self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists() + form.show_contact_as_plain_text = self.is_omb_analyst + form.is_omb_analyst = self.is_omb_analyst + + return form + + def get_formset(self, request, obj=None, **kwargs): + """Attach request to the formset so that it can be available in the form""" + formset = super().get_formset(request, obj, **kwargs) + formset.form.request = request # Attach request to form + return formset + class DomainResource(FsmModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -3454,7 +3804,7 @@ class DomainResource(FsmModelResource): model = models.Domain -class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class DomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Custom domain admin class to add extra buttons.""" resource_classes = [DomainResource] @@ -3566,7 +3916,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): if self.value(): return queryset.filter( Q(domain_info__portfolio__federal_type=self.value()) - | Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value()) + | Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value()) ) return queryset @@ -3593,7 +3943,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False), then=F("domain_info__portfolio__federal_agency__federal_type"), ), - # Otherwise, return the natively assigned value + # Otherwise, return federal type from federal agency default=F("domain_info__federal_agency__federal_type"), ), converted_organization_name=Case( @@ -4020,8 +4370,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Fixes a bug wherein users which are only is_staff # can access 'change' when GET, # but cannot access this page when it is a request of type POST. - if request.user.has_perm("registrar.full_access_permission") or request.user.has_perm( - "registrar.analyst_access_permission" + if ( + request.user.has_perm("registrar.full_access_permission") + or request.user.has_perm("registrar.analyst_access_permission") + or request.user.groups.filter(name="omb_analysts_group").exists() ): return True return super().has_change_permission(request, obj) @@ -4036,8 +4388,37 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): if portfolio_id: # Further filter the queryset by the portfolio qs = qs.filter(domain_info__portfolio=portfolio_id) + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + return qs.filter( + converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + converted_federal_type=BranchChoices.EXECUTIVE, + ) return qs + def has_view_permission(self, request, obj=None): + """Restrict view permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return ( + obj.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL + and obj.domain_info.converted_federal_type == BranchChoices.EXECUTIVE + ) + return super().has_view_permission(request, obj) + + def get_form(self, request, obj=None, **kwargs): + """Pass the 'is_omb_analyst' attribute to the form.""" + form = super().get_form(request, obj, **kwargs) + + # Store attribute in the form for template access + is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists() + form.show_contact_as_plain_text = is_omb_analyst + form.is_omb_analyst = is_omb_analyst + + return form + class DraftDomainResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -4047,7 +4428,7 @@ class DraftDomainResource(resources.ModelResource): model = models.DraftDomain -class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class DraftDomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Custom draft domain admin class.""" resource_classes = [DraftDomainResource] @@ -4159,7 +4540,7 @@ class PublicContactResource(resources.ModelResource): self.after_save_instance(instance, using_transactions, dry_run) -class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class PublicContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): """Custom PublicContact admin class.""" resource_classes = [PublicContactResource] @@ -4214,6 +4595,11 @@ class PortfolioAdmin(ListHeaderAdmin): _meta = Meta() + def __init__(self, *args, **kwargs): + """Initialize the admin class and define a default value for is_omb_analyst.""" + super().__init__(*args, **kwargs) + self.is_omb_analyst = False # Default value in case it's accessed before being set + change_form_template = "django/admin/portfolio_change_form.html" fieldsets = [ # created_on is the created_at field @@ -4295,6 +4681,19 @@ class PortfolioAdmin(ListHeaderAdmin): # rather than strip it out of our logic. analyst_readonly_fields = [] # type: ignore + omb_analyst_readonly_fields = [ + "notes", + "organization_type", + "organization_name", + "federal_agency", + "state_territory", + "address_line1", + "address_line2", + "city", + "zipcode", + "urbanization", + ] + def get_admin_users(self, obj): # Filter UserPortfolioPermission objects related to the portfolio admin_permissions = self.get_user_portfolio_permission_admins(obj) @@ -4380,6 +4779,8 @@ class PortfolioAdmin(ListHeaderAdmin): """Returns the number of administrators for this portfolio""" admin_count = len(self.get_user_portfolio_permission_admins(obj)) if admin_count > 0: + if self.is_omb_analyst: + return format_html(f"{admin_count} administrators") url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}" # Create a clickable link with the count return format_html(f'{admin_count} admins') @@ -4391,6 +4792,8 @@ class PortfolioAdmin(ListHeaderAdmin): """Returns the number of basic members for this portfolio""" member_count = len(self.get_user_portfolio_permission_non_admins(obj)) if member_count > 0: + if self.is_omb_analyst: + return format_html(f"{member_count} members") url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}" # Create a clickable link with the count return format_html(f'{member_count} basic members') @@ -4436,12 +4839,35 @@ class PortfolioAdmin(ListHeaderAdmin): if request.user.has_perm("registrar.full_access_permission"): return readonly_fields - + # Return restrictive Read-only fields for OMB analysts + if request.user.groups.filter(name="omb_analysts_group").exists(): + readonly_fields.extend([field for field in self.omb_analyst_readonly_fields]) + return readonly_fields # Return restrictive Read-only fields for analysts and # users who might not belong to groups readonly_fields.extend([field for field in self.analyst_readonly_fields]) return readonly_fields + def get_queryset(self, request): + """Restrict queryset based on user permissions.""" + qs = super().get_queryset(request) + + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + self.is_omb_analyst = True + return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE) + + return qs # Return full queryset if the user doesn't have the restriction + + def has_view_permission(self, request, obj=None): + """Restrict view permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return obj.federal_type == BranchChoices.EXECUTIVE + return super().has_view_permission(request, obj) + def change_view(self, request, object_id, form_url="", extra_context=None): """Add related suborganizations and domain groups. Add the summary for the portfolio members field (list of members that link to change_forms).""" @@ -4486,6 +4912,17 @@ class PortfolioAdmin(ListHeaderAdmin): super().save_model(request, obj, form, change) + def get_form(self, request, obj=None, **kwargs): + """Pass the 'is_omb_analyst' attribute to the form.""" + form = super().get_form(request, obj, **kwargs) + + # Store attribute in the form for template access + self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists() + form.show_contact_as_plain_text = self.is_omb_analyst + form.is_omb_analyst = self.is_omb_analyst + + return form + class FederalAgencyResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -4495,13 +4932,66 @@ class FederalAgencyResource(resources.ModelResource): model = models.FederalAgency -class FederalAgencyAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): list_display = ["agency"] search_fields = ["agency"] search_help_text = "Search by federal agency." ordering = ["agency"] resource_classes = [FederalAgencyResource] + # Readonly fields for analysts and superusers + readonly_fields = [] + + # Read only that we'll leverage for CISA Analysts + analyst_readonly_fields = [] # type: ignore + + # Read only that we'll leverage for OMB Analysts + omb_analyst_readonly_fields = [ + "agency", + "federal_type", + "acronym", + "is_fceb", + ] + + def get_queryset(self, request): + """Restrict queryset based on user permissions.""" + qs = super().get_queryset(request) + + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + return qs.filter( + federal_type=BranchChoices.EXECUTIVE, + ) + + return qs # Return full queryset if the user doesn't have the restriction + + def has_view_permission(self, request, obj=None): + """Restrict view permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return obj.federal_type == BranchChoices.EXECUTIVE + return super().has_view_permission(request, obj) + + def get_readonly_fields(self, request, obj=None): + """Set the read-only state on form elements. + We have 2 conditions that determine which fields are read-only: + admin user permissions and the domain request creator's status, so + we'll use the baseline readonly_fields and extend it as needed. + """ + readonly_fields = list(self.readonly_fields) + if request.user.has_perm("registrar.full_access_permission"): + return readonly_fields + # Return restrictive Read-only fields for OMB analysts + if request.user.groups.filter(name="omb_analysts_group").exists(): + readonly_fields.extend([field for field in self.omb_analyst_readonly_fields]) + return readonly_fields + # Return restrictive Read-only fields for analysts and + # users who might not belong to groups + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields + class UserGroupAdmin(AuditedAdmin): """Overwrite the generated UserGroup admin class""" @@ -4551,11 +5041,11 @@ class WaffleFlagAdmin(FlagAdmin): return super().changelist_view(request, extra_context=extra_context) -class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class DomainGroupAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): list_display = ["name", "portfolio"] -class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): +class SuborganizationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin): list_display = ["name", "portfolio"] autocomplete_fields = [ @@ -4566,6 +5056,38 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): change_form_template = "django/admin/suborg_change_form.html" + readonly_fields = [] + + # Even though this is empty, I will leave it as a stub for easy changes in the future + # rather than strip it out of our logic. + analyst_readonly_fields = [] # type: ignore + + omb_analyst_readonly_fields = [ + "name", + "portfolio", + "city", + "state_territory", + ] + + def get_readonly_fields(self, request, obj=None): + """Set the read-only state on form elements. + We have conditions that determine which fields are read-only: + admin user permissions and analyst (cisa or omb) status, so + we'll use the baseline readonly_fields and extend it as needed. + """ + readonly_fields = list(self.readonly_fields) + + if request.user.has_perm("registrar.full_access_permission"): + return readonly_fields + # Return restrictive Read-only fields for OMB analysts + if request.user.groups.filter(name="omb_analysts_group").exists(): + readonly_fields.extend([field for field in self.omb_analyst_readonly_fields]) + return readonly_fields + # Return restrictive Read-only fields for analysts and + # users who might not belong to groups + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields + def change_view(self, request, object_id, form_url="", extra_context=None): """Add suborg's related domains and requests to context""" obj = self.get_object(request, object_id) @@ -4583,6 +5105,30 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): extra_context = {"domain_requests": domain_requests, "domains": domains} return super().change_view(request, object_id, form_url, extra_context) + def get_queryset(self, request): + """Custom get_queryset to filter for OMB analysts.""" + qs = super().get_queryset(request) + # Check if user is in OMB analysts group + if request.user.groups.filter(name="omb_analysts_group").exists(): + return qs.filter( + portfolio__organization_type=DomainRequest.OrganizationChoices.FEDERAL, + portfolio__federal_agency__federal_type=BranchChoices.EXECUTIVE, + ) + return qs + + def has_view_permission(self, request, obj=None): + """Restrict view permissions based on group membership and model attributes.""" + if request.user.has_perm("registrar.full_access_permission"): + return True + if obj: + if request.user.groups.filter(name="omb_analysts_group").exists(): + return ( + obj.portfolio + and obj.portfolio.federal_agency + and obj.portfolio.federal_agency.federal_type == BranchChoices.EXECUTIVE + ) + return super().has_view_permission(request, obj) + class AllowedEmailAdmin(ListHeaderAdmin): class Meta: diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js index db6467875..b9084494a 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js @@ -105,8 +105,10 @@ export function initApprovedDomain() { return; } - const statusToCheck = "approved"; + const statusToCheck = "approved"; // when checking against a select + const readonlyStatusToCheck = "Approved"; // when checking against a readonly div display value const statusSelect = document.getElementById("id_status"); + const statusField = document.querySelector("field-status"); const sessionVariableName = "showApprovedDomain"; let approvedDomainFormGroup = document.querySelector(".field-approved_domain"); @@ -120,18 +122,32 @@ export function initApprovedDomain() { // Handle showing/hiding the related fields on page load. function initializeFormGroups() { - let isStatus = statusSelect.value == statusToCheck; + // Status is either in a select or in a readonly div. Both + // cases are handled below. + let isStatus = false; + if (statusSelect) { + isStatus = statusSelect.value == statusToCheck; + } else { + // statusSelect does not exist, indicating readonly + if (statusField) { + let readonlyDiv = statusField.querySelector("div.readonly"); + let readonlyStatusText = readonlyDiv.textContent.trim(); + isStatus = readonlyStatusText == readonlyStatusToCheck; + } + } // Initial handling of these groups. updateFormGroupVisibility(isStatus); - // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage - statusSelect.addEventListener('change', () => { - // Show the approved if the status is what we expect. - isStatus = statusSelect.value == statusToCheck; - updateFormGroupVisibility(isStatus); - addOrRemoveSessionBoolean(sessionVariableName, isStatus); - }); + if (statusSelect) { + // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage + statusSelect.addEventListener('change', () => { + // Show the approved if the status is what we expect. + isStatus = statusSelect.value == statusToCheck; + updateFormGroupVisibility(isStatus); + addOrRemoveSessionBoolean(sessionVariableName, isStatus); + }); + } // Listen to Back/Forward button navigation and handle approvedDomainFormGroup display based on session storage // When you navigate using forward/back after changing status but not saving, when you land back on the DA page the @@ -322,6 +338,7 @@ class CustomizableEmailBase { * @property {HTMLElement} modalConfirm - The confirm button in the modal. * @property {string} apiUrl - The API URL for fetching email content. * @property {string} statusToCheck - The status to check against. Used for show/hide on textAreaFormGroup/dropdownFormGroup. + * @property {string} readonlyStatusToCheck - The status to check against when readonly. Used for show/hide on textAreaFormGroup/dropdownFormGroup. * @property {string} sessionVariableName - The session variable name. Used for show/hide on textAreaFormGroup/dropdownFormGroup. * @property {string} apiErrorMessage - The error message that the ajax call returns. */ @@ -338,6 +355,7 @@ class CustomizableEmailBase { this.textAreaFormGroup = config.textAreaFormGroup; this.dropdownFormGroup = config.dropdownFormGroup; this.statusToCheck = config.statusToCheck; + this.readonlyStatusToCheck = config.readonlyStatusToCheck; this.sessionVariableName = config.sessionVariableName; // Non-configurable variables @@ -363,19 +381,31 @@ class CustomizableEmailBase { // Handle showing/hiding the related fields on page load. initializeFormGroups() { - let isStatus = this.statusSelect.value == this.statusToCheck; + let isStatus = false; + if (this.statusSelect) { + isStatus = this.statusSelect.value == this.statusToCheck; + } else { + // statusSelect does not exist, indicating readonly + if (this.dropdownFormGroup) { + let readonlyDiv = this.dropdownFormGroup.querySelector("div.readonly"); + let readonlyStatusText = readonlyDiv.textContent.trim(); + isStatus = readonlyStatusText == this.readonlyStatusToCheck; + } + } // Initial handling of these groups. this.updateFormGroupVisibility(isStatus); - // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage - this.statusSelect.addEventListener('change', () => { - // Show the action needed field if the status is what we expect. - // Then track if its shown or hidden in our session cache. - isStatus = this.statusSelect.value == this.statusToCheck; - this.updateFormGroupVisibility(isStatus); - addOrRemoveSessionBoolean(this.sessionVariableName, isStatus); - }); + if (this.statusSelect) { + // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage + this.statusSelect.addEventListener('change', () => { + // Show the action needed field if the status is what we expect. + // Then track if its shown or hidden in our session cache. + isStatus = this.statusSelect.value == this.statusToCheck; + this.updateFormGroupVisibility(isStatus); + addOrRemoveSessionBoolean(this.sessionVariableName, isStatus); + }); + } // Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage // When you navigate using forward/back after changing status but not saving, when you land back on the DA page the @@ -403,58 +433,66 @@ class CustomizableEmailBase { } initializeDropdown() { - this.dropdown.addEventListener("change", () => { - let reason = this.dropdown.value; - if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) { - let searchParams = new URLSearchParams( - { - "reason": reason, - "domain_request_id": this.domainRequestId, - } - ); - // Replace the email content - fetch(`${this.apiUrl}?${searchParams.toString()}`) - .then(response => { - return response.json().then(data => data); - }) - .then(data => { - if (data.error) { - console.error("Error in AJAX call: " + data.error); - }else { - this.textarea.value = data.email; - } - this.updateUserInterface(reason); - }) - .catch(error => { - console.error(this.apiErrorMessage, error) - }); - } - }); + if (this.dropdown) { + this.dropdown.addEventListener("change", () => { + let reason = this.dropdown.value; + if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) { + let searchParams = new URLSearchParams( + { + "reason": reason, + "domain_request_id": this.domainRequestId, + } + ); + // Replace the email content + fetch(`${this.apiUrl}?${searchParams.toString()}`) + .then(response => { + return response.json().then(data => data); + }) + .then(data => { + if (data.error) { + console.error("Error in AJAX call: " + data.error); + }else { + this.textarea.value = data.email; + } + this.updateUserInterface(reason); + }) + .catch(error => { + console.error(this.apiErrorMessage, error) + }); + } + }); + } } initializeModalConfirm() { - this.modalConfirm.addEventListener("click", () => { - this.textarea.removeAttribute('readonly'); - this.textarea.focus(); + // When the modal confirm button is present, add a listener + if (this.modalConfirm) { + this.modalConfirm.addEventListener("click", () => { + this.textarea.removeAttribute('readonly'); + this.textarea.focus(); hideElement(this.directEditButton); hideElement(this.modalTrigger); - }); + }); + } } initializeDirectEditButton() { - this.directEditButton.addEventListener("click", () => { - this.textarea.removeAttribute('readonly'); - this.textarea.focus(); + // When the direct edit button is present, add a listener + if (this.directEditButton) { + this.directEditButton.addEventListener("click", () => { + this.textarea.removeAttribute('readonly'); + this.textarea.focus(); hideElement(this.directEditButton); hideElement(this.modalTrigger); - }); + }); + } } isEmailAlreadySent() { return this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, ''); } - updateUserInterface(reason=this.dropdown.value, excluded_reasons=["other"]) { + updateUserInterface(reason, excluded_reasons=["other"]) { if (!reason) { // No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text this.showPlaceholderNoReason(); @@ -468,23 +506,25 @@ class CustomizableEmailBase { // Helper function that makes overriding the readonly textarea easy showReadonlyTextarea() { - // A triggering selection is selected, all hands on board: - this.textarea.setAttribute('readonly', true); - showElement(this.textarea); - hideElement(this.textareaPlaceholder); + if (this.textarea && this.textareaPlaceholder) { + // A triggering selection is selected, all hands on board: + this.textarea.setAttribute('readonly', true); + showElement(this.textarea); + hideElement(this.textareaPlaceholder); - if (this.isEmailAlreadySentConst) { - hideElement(this.directEditButton); - showElement(this.modalTrigger); + if (this.isEmailAlreadySentConst) { + hideElement(this.directEditButton); + showElement(this.modalTrigger); + } else { + showElement(this.directEditButton); + hideElement(this.modalTrigger); + } + + if (this.isEmailAlreadySent()) { + this.formLabel.innerHTML = "Email sent to creator:"; } else { - showElement(this.directEditButton); - hideElement(this.modalTrigger); - } - - if (this.isEmailAlreadySent()) { - this.formLabel.innerHTML = "Email sent to creator:"; - } else { - this.formLabel.innerHTML = "Email:"; + this.formLabel.innerHTML = "Email:"; + } } } @@ -516,9 +556,10 @@ class customActionNeededEmail extends CustomizableEmailBase { lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"), modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"), apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null, - textAreaFormGroup: document.querySelector('.field-action_needed_reason'), - dropdownFormGroup: document.querySelector('.field-action_needed_reason_email'), + textAreaFormGroup: document.querySelector('.field-action_needed_reason_email'), + dropdownFormGroup: document.querySelector('.field-action_needed_reason'), statusToCheck: "action needed", + readonlyStatusToCheck: "Action needed", sessionVariableName: "showActionNeededReason", apiErrorMessage: "Error when attempting to grab action needed email: " } @@ -529,7 +570,15 @@ class customActionNeededEmail extends CustomizableEmailBase { // Hide/show the email fields depending on the current status this.initializeFormGroups(); // Setup the textarea, edit button, helper text - this.updateUserInterface(); + let reason = null; + if (this.dropdown) { + reason = this.dropdown.value; + } else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) { + if (this.dropdownFormGroup.querySelector("div.readonly").textContent) { + reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim() + } + } + this.updateUserInterface(reason); this.initializeDropdown(); this.initializeModalConfirm(); this.initializeDirectEditButton(); @@ -560,12 +609,6 @@ export function initActionNeededEmail() { // Initialize UI const customEmail = new customActionNeededEmail(); - // Check that every variable was setup correctly - const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key); - if (nullItems.length > 0) { - console.error(`Failed to load customActionNeededEmail(). Some variables were null: ${nullItems.join(", ")}`) - return; - } customEmail.loadActionNeededEmail() }); } @@ -581,6 +624,7 @@ class customRejectedEmail extends CustomizableEmailBase { textAreaFormGroup: document.querySelector('.field-rejection_reason'), dropdownFormGroup: document.querySelector('.field-rejection_reason_email'), statusToCheck: "rejected", + readonlyStatusToCheck: "Rejected", sessionVariableName: "showRejectionReason", errorMessage: "Error when attempting to grab rejected email: " }; @@ -589,7 +633,15 @@ class customRejectedEmail extends CustomizableEmailBase { loadRejectedEmail() { this.initializeFormGroups(); - this.updateUserInterface(); + let reason = null; + if (this.dropdown) { + reason = this.dropdown.value; + } else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) { + if (this.dropdownFormGroup.querySelector("div.readonly").textContent) { + reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim() + } + } + this.updateUserInterface(reason); this.initializeDropdown(); this.initializeModalConfirm(); this.initializeDirectEditButton(); @@ -600,7 +652,7 @@ class customRejectedEmail extends CustomizableEmailBase { this.showPlaceholder("Email:", "Select a rejection reason to see email"); } - updateUserInterface(reason=this.dropdown.value, excluded_reasons=[]) { + updateUserInterface(reason, excluded_reasons=[]) { super.updateUserInterface(reason, excluded_reasons); } } @@ -619,12 +671,6 @@ export function initRejectedEmail() { // Initialize UI const customEmail = new customRejectedEmail(); - // Check that every variable was setup correctly - const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key); - if (nullItems.length > 0) { - console.error(`Failed to load customRejectedEmail(). Some variables were null: ${nullItems.join(", ")}`) - return; - } customEmail.loadRejectedEmail() }); } @@ -648,7 +694,6 @@ function handleSuborgFieldsAndButtons() { // Ensure that every variable is present before proceeding if (!requestedSuborganizationField || !suborganizationCity || !suborganizationStateTerritory || !rejectButton) { - console.warn("handleSuborganizationSelection() => Could not find required fields.") return; } diff --git a/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js b/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js index 9a60e1684..3880e63fa 100644 --- a/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js +++ b/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js @@ -12,7 +12,9 @@ export function handlePortfolioSelection( suborgDropdownSelector="#id_sub_organization" ) { // These dropdown are select2 fields so they must be interacted with via jquery + // In the event that these fields are readonly, need a variable to reference their row const portfolioDropdown = django.jQuery(portfolioDropdownSelector); + const portfolioField = document.querySelector(".field-portfolio"); const suborganizationDropdown = django.jQuery(suborgDropdownSelector); const suborganizationField = document.querySelector(".field-sub_organization"); const requestedSuborganizationField = document.querySelector(".field-requested_suborganization"); @@ -394,17 +396,33 @@ export function handlePortfolioSelection( * - Various global field elements (e.g., `suborganizationField`, `seniorOfficialField`, `portfolioOrgTypeFieldSet`) are used. */ function updatePortfolioFieldsDisplay() { - // Retrieve the selected portfolio ID - let portfolio_id = portfolioDropdown.val(); + let portfolio_id = null; + let portfolio_selected = false; + // portfolio will be either readonly or a dropdown, handle both cases + if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM + // Retrieve the selected portfolio ID + portfolio_id = portfolioDropdown.val(); + if (portfolio_id) { + portfolio_selected = true; + } + } else { + // get readonly field value + let portfolio = portfolioField.querySelector(".readonly").innerText; + if (portfolio != "-") { + portfolio_selected = true; + } + } - if (portfolio_id) { + if (portfolio_selected) { // A portfolio is selected - update suborganization dropdown and show/hide relevant fields - // Update suborganization dropdown for the selected portfolio - updateSubOrganizationDropdown(portfolio_id); + if (portfolio_id) { + // Update suborganization dropdown for the selected portfolio + updateSubOrganizationDropdown(portfolio_id); + } // Show fields relevant to a selected portfolio - showElement(suborganizationField); + if (suborganizationField) showElement(suborganizationField); hideElement(seniorOfficialField); showElement(portfolioSeniorOfficialField); @@ -427,7 +445,7 @@ export function handlePortfolioSelection( // No portfolio is selected - reverse visibility of fields // Hide suborganization field as no portfolio is selected - hideElement(suborganizationField); + if (suborganizationField) hideElement(suborganizationField); // Show fields that are relevant when no portfolio is selected showElement(seniorOfficialField); @@ -468,10 +486,22 @@ export function handlePortfolioSelection( * This function ensures the form dynamically reflects whether a specific suborganization is being selected or requested. */ function updateSuborganizationFieldsDisplay() { - let portfolio_id = portfolioDropdown.val(); + let portfolio_selected = false; + // portfolio will be either readonly or a dropdown, handle both cases + if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM + // Retrieve the selected portfolio ID + if (portfolioDropdown.val()) { + portfolio_selected = true; + } + } else { + // get readonly field value + if (portfolioField.querySelector(".readonly").innerText != "-") { + portfolio_selected = true; + } + } let suborganization_id = suborganizationDropdown.val(); - if (portfolio_id && !suborganization_id) { + if (portfolio_selected && !suborganization_id) { // Show suborganization request fields if (requestedSuborganizationField) showElement(requestedSuborganizationField); if (suborganizationCity) showElement(suborganizationCity); diff --git a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js index 74729c2b2..7777dabdb 100644 --- a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js +++ b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js @@ -21,6 +21,8 @@ function handlePortfolioFields(){ const federalTypeField = document.querySelector(".field-federal_type"); const urbanizationField = document.querySelector(".field-urbanization"); const stateTerritoryDropdown = document.getElementById("id_state_territory"); + const stateTerritoryField = document.querySelector(".field-state_territory"); + const stateTerritoryReadonly = stateTerritoryField.querySelector(".readonly"); const seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value; const seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value; const federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value; @@ -85,9 +87,9 @@ function handlePortfolioFields(){ * 2. else show org name, hide federal agency, hide federal type if applicable */ function handleOrganizationTypeChange() { - if (organizationTypeDropdown && organizationNameField) { - let selectedValue = organizationTypeDropdown.value; - if (selectedValue === "federal") { + if (organizationTypeField && organizationNameField) { + let selectedValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText; + if (selectedValue === "federal" || selectedValue === "Federal") { hideElement(organizationNameField); showElement(federalAgencyField); if (federalTypeField) { @@ -207,8 +209,8 @@ function handlePortfolioFields(){ * Handle urbanization */ function handleStateTerritoryChange() { - let selectedValue = stateTerritoryDropdown.value; - if (selectedValue === "PR") { + let selectedValue = stateTerritoryDropdown ? stateTerritoryDropdown.value : stateTerritoryReadonly.innerText; + if (selectedValue === "PR" || selectedValue === "Puerto Rico (PR)") { showElement(urbanizationField) } else { hideElement(urbanizationField) @@ -265,7 +267,7 @@ function handlePortfolioFields(){ * Initializes necessary data and display configurations for the portfolio fields. */ function initializePortfolioSettings() { - if (urbanizationField && stateTerritoryDropdown) { + if (urbanizationField && stateTerritoryField) { handleStateTerritoryChange(); } handleOrganizationTypeChange(); @@ -285,9 +287,11 @@ function handlePortfolioFields(){ handleStateTerritoryChange(); }); } - organizationTypeDropdown.addEventListener("change", function() { - handleOrganizationTypeChange(); - }); + if (organizationTypeDropdown) { + organizationTypeDropdown.addEventListener("change", function() { + handleOrganizationTypeChange(); + }); + } } // Run initial setup functions diff --git a/src/registrar/assets/src/js/getgov/domain-dsdata.js b/src/registrar/assets/src/js/getgov/domain-dsdata.js deleted file mode 100644 index 14132d812..000000000 --- a/src/registrar/assets/src/js/getgov/domain-dsdata.js +++ /dev/null @@ -1,27 +0,0 @@ -import { submitForm } from './form-helpers.js'; - -export function initDomainDSData() { - document.addEventListener('DOMContentLoaded', function() { - let domain_dsdata_page = document.getElementById("domain-dsdata"); - if (domain_dsdata_page) { - const override_button = document.getElementById("disable-override-click-button"); - const cancel_button = document.getElementById("btn-cancel-click-button"); - const cancel_close_button = document.getElementById("btn-cancel-click-close-button"); - if (override_button) { - override_button.addEventListener("click", function () { - submitForm("disable-override-click-form"); - }); - } - if (cancel_button) { - cancel_button.addEventListener("click", function () { - submitForm("btn-cancel-click-form"); - }); - } - if (cancel_close_button) { - cancel_close_button.addEventListener("click", function () { - submitForm("btn-cancel-click-form"); - }); - } - } - }); -} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/form-dsdata.js b/src/registrar/assets/src/js/getgov/form-dsdata.js new file mode 100644 index 000000000..6d05dfa9b --- /dev/null +++ b/src/registrar/assets/src/js/getgov/form-dsdata.js @@ -0,0 +1,521 @@ +import { showElement, hideElement, scrollToElement } from './helpers'; +import { removeErrorsFromElement, removeFormErrors } from './form-helpers'; + +export class DSDataForm { + constructor() { + this.addDSDataButton = document.getElementById('dsdata-add-button'); + this.addDSDataForm = document.querySelector('.add-dsdata-form'); + this.formChanged = false; + this.callback = null; + + // Bind event handlers to maintain 'this' context + this.handleAddFormClick = this.handleAddFormClick.bind(this); + this.handleEditClick = this.handleEditClick.bind(this); + this.handleDeleteClick = this.handleDeleteClick.bind(this); + this.handleDeleteKebabClick = this.handleDeleteKebabClick.bind(this); + this.handleCancelClick = this.handleCancelClick.bind(this); + this.handleCancelAddFormClick = this.handleCancelAddFormClick.bind(this); + } + + /** + * Initialize the DSDataForm by setting up display and event listeners. + */ + init() { + this.initializeDSDataFormDisplay(); + this.initializeEventListeners(); + } + + + /** + * Determines the initial display state of the DS data form, + * handling validation errors and setting visibility of elements accordingly. + */ + initializeDSDataFormDisplay() { + + // This check indicates that there is an Add DS Data form + // and that form has errors in it. In this case, show the form, and indicate that the form has + // changed. + if (this.addDSDataForm && this.addDSDataForm.querySelector('.usa-input--error')) { + showElement(this.addDSDataForm); + this.formChanged = true; + } + + // handle display of table view errors + // if error exists in an edit-row, make that row show, and readonly row hide + const formTable = document.getElementById('dsdata-table') + if (formTable) { + const editRows = formTable.querySelectorAll('.edit-row'); + editRows.forEach(editRow => { + if (editRow.querySelector('.usa-input--error')) { + const readOnlyRow = editRow.previousElementSibling; + this.formChanged = true; + showElement(editRow); + hideElement(readOnlyRow); + } + }) + } + + } + + /** + * Attaches event listeners to relevant UI elements for interaction handling. + */ + initializeEventListeners() { + this.addDSDataButton.addEventListener('click', this.handleAddFormClick); + + const editButtons = document.querySelectorAll('.dsdata-edit'); + editButtons.forEach(editButton => { + editButton.addEventListener('click', this.handleEditClick); + }); + + const cancelButtons = document.querySelectorAll('.dsdata-cancel'); + cancelButtons.forEach(cancelButton => { + cancelButton.addEventListener('click', this.handleCancelClick); + }); + + const cancelAddFormButtons = document.querySelectorAll('.dsdata-cancel-add-form'); + cancelAddFormButtons.forEach(cancelAddFormButton => { + cancelAddFormButton.addEventListener('click', this.handleCancelAddFormClick); + }); + + const deleteButtons = document.querySelectorAll('.dsdata-delete'); + deleteButtons.forEach(deleteButton => { + deleteButton.addEventListener('click', this.handleDeleteClick); + }); + + const deleteKebabButtons = document.querySelectorAll('.dsdata-delete-kebab'); + deleteKebabButtons.forEach(deleteKebabButton => { + deleteKebabButton.addEventListener('click', this.handleDeleteKebabClick); + }); + + const inputs = document.querySelectorAll("input[type='text'], textarea"); + inputs.forEach(input => { + input.addEventListener("input", () => { + this.formChanged = true; + }); + }); + + const selects = document.querySelectorAll("select"); + selects.forEach(select => { + select.addEventListener("change", () => { + this.formChanged = true; + }); + }); + + // Set event listeners on the submit buttons for the modals. Event listeners + // should execute the callback function, which has its logic updated prior + // to modal display + const unsaved_changes_modal = document.getElementById('unsaved-changes-modal'); + if (unsaved_changes_modal) { + const submitButton = document.getElementById('unsaved-changes-click-button'); + const closeButton = unsaved_changes_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } + const cancel_changes_modal = document.getElementById('cancel-changes-modal'); + if (cancel_changes_modal) { + const submitButton = document.getElementById('cancel-changes-click-button'); + const closeButton = cancel_changes_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } + const delete_modal = document.getElementById('delete-modal'); + if (delete_modal) { + const submitButton = document.getElementById('delete-click-button'); + const closeButton = delete_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } + const disable_dnssec_modal = document.getElementById('disable-dnssec-modal'); + if (disable_dnssec_modal) { + const submitButton = document.getElementById('disable-dnssec-click-button'); + const closeButton = disable_dnssec_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } + + } + + /** + * Executes a stored callback function if defined, otherwise logs a warning. + */ + executeCallback() { + if (this.callback) { + this.callback(); + this.callback = null; + } else { + console.warn("No callback function set."); + } + } + + /** + * Handles clicking the 'Add DS data' button, showing the form if needed. + * @param {Event} event - Click event + */ + handleAddFormClick(event) { + this.callback = () => { + // Check if any other edit row is currently visible and hide it + document.querySelectorAll('tr.edit-row:not(.display-none)').forEach(openEditRow => { + this.resetEditRowAndFormAndCollapseEditRow(openEditRow); + }); + if (this.addDSDataForm) { + // Check if this.addDSDataForm is visible (i.e., does not have 'display-none') + if (!this.addDSDataForm.classList.contains('display-none')) { + this.resetAddDSDataForm(); + } + // show add ds data form + showElement(this.addDSDataForm); + // focus on key tag in the form + let keyTagInput = this.addDSDataForm.querySelector('input[name$="-key_tag"]'); + if (keyTagInput) { + keyTagInput.focus(); + } + } else { + this.addAlert("error", "You’ve reached the maximum amount of DS Data records (8). To add another record, you’ll need to delete one of your saved records."); + } + }; + if (this.formChanged) { + //------- Show the unsaved changes confirmation modal + let modalTrigger = document.querySelector("#unsaved_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } + } + + /** + * Handles clicking an 'Edit' button on a readonly row, which hides the readonly row + * and displays the edit row, after performing some checks and possibly displaying modal. + * @param {Event} event - Click event + */ + handleEditClick(event) { + let editButton = event.target; + let readOnlyRow = editButton.closest('tr'); // Find the closest row + let editRow = readOnlyRow.nextElementSibling; // Get the next row + if (!editRow || !readOnlyRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + this.callback = () => { + // Check if any other edit row is currently visible and hide it + document.querySelectorAll('tr.edit-row:not(.display-none)').forEach(openEditRow => { + this.resetEditRowAndFormAndCollapseEditRow(openEditRow); + }); + // Check if this.addDSDataForm is visible (i.e., does not have 'display-none') + if (this.addDSDataForm && !this.addDSDataForm.classList.contains('display-none')) { + this.resetAddDSDataForm(); + } + // hide and show rows as appropriate + hideElement(readOnlyRow); + showElement(editRow); + }; + if (this.formChanged) { + //------- Show the unsaved changes confirmation modal + let modalTrigger = document.querySelector("#unsaved_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } + } + + /** + * Handles clicking a 'Delete' button on an edit row, which hattempts to delete the DS record + * after displaying modal. + * @param {Event} event - Click event + */ + handleDeleteClick(event) { + let deleteButton = event.target; + let editRow = deleteButton.closest('tr'); + if (!editRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + this.deleteRow(editRow); + } + + /** + * Handles clicking a 'Delete' button on a readonly row in a kebab, which attempts to delete the DS record + * after displaying modal. + * @param {Event} event - Click event + */ + handleDeleteKebabClick(event) { + let deleteKebabButton = event.target; + let accordionDiv = deleteKebabButton.closest('div'); + // hide the accordion + accordionDiv.hidden = true; + let readOnlyRow = deleteKebabButton.closest('tr'); // Find the closest row + let editRow = readOnlyRow.nextElementSibling; // Get the next row + if (!editRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + this.deleteRow(editRow); + } + + /** + * Deletes a DS record row. If there is only one DS record, prompt the user + * that they will be disabling DNSSEC. Otherwise, prompt with delete confiration. + * If deletion proceeds, the input fields are cleared, and the form is submitted. + * @param {HTMLElement} editRow - The row corresponding to the DS record being deleted. + */ + deleteRow(editRow) { + // update the callback method + this.callback = () => { + hideElement(editRow); + let deleteInput = editRow.querySelector("input[name$='-DELETE']"); + if (deleteInput) { + deleteInput.checked = true; + } + document.querySelector("form").submit(); + }; + // Check if at least 2 DS data records exist before the delete row action is taken + const thirdDSData = document.getElementById('id_form-2-key_tag') + if (thirdDSData) { + let modalTrigger = document.querySelector('#delete_trigger'); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + let modalTrigger = document.querySelector('#disable_dnssec_trigger'); + if (modalTrigger) { + modalTrigger.click(); + } + } + } + + /** + * Handles the click event on the "Cancel" button in the add DS data form. + * Resets the form fields and hides the add form section. + * @param {Event} event - Click event + */ + handleCancelAddFormClick(event) { + this.callback = () => { + this.resetAddDSDataForm(); + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } + } + + /** + * Handles the click event for the cancel button within the table form. + * + * This method identifies the edit row containing the cancel button and resets + * it to its initial state, restoring the corresponding read-only row. + * + * @param {Event} event - the click event triggered by the cancel button + */ + handleCancelClick(event) { + // get the cancel button that was clicked + let cancelButton = event.target; + // find the closest table row that contains the cancel button + let editRow = cancelButton.closest('tr'); + this.callback = () => { + if (editRow) { + this.resetEditRowAndFormAndCollapseEditRow(editRow); + } else { + console.warn("Expected DOM element but did not find it"); + } + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } + } + + /** + * Resets the edit row, restores its original values, removes validation errors, + * and collapses the edit row while making the readonly row visible again. + * @param {HTMLElement} editRow - The row that is being reset and collapsed. + */ + resetEditRowAndFormAndCollapseEditRow(editRow) { + let readOnlyRow = editRow.previousElementSibling; // Get the next row + if (!editRow || !readOnlyRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + // reset the values set in editRow + this.resetInputValuesInElement(editRow); + // copy values from editRow to readOnlyRow + this.copyEditRowToReadonlyRow(editRow, readOnlyRow); + // remove errors from the editRow + removeErrorsFromElement(editRow); + // remove errors from the entire form + removeFormErrors(); + // reset formChanged + this.resetFormChanged(); + // hide and show rows as appropriate + hideElement(editRow); + showElement(readOnlyRow); + } + + /** + * Resets the 'Add DS data' form by clearing its input fields, removing errors, + * and hiding the form to return it to its initial state. + */ + resetAddDSDataForm() { + if (this.addDSDataForm) { + // reset the values set in addDSDataForm + this.resetInputValuesInElement(this.addDSDataForm); + // remove errors from the addDSDataForm + removeErrorsFromElement(this.addDSDataForm); + // remove errors from the entire form + removeFormErrors(); + // reset formChanged + this.resetFormChanged(); + // hide the addDSDataForm + hideElement(this.addDSDataForm); + } + } + + /** + * Resets all text input fields within the specified DOM element to their initial values. + * Triggers an 'input' event to ensure any event listeners update accordingly. + * @param {HTMLElement} domElement - The parent element containing text input fields to be reset. + */ + resetInputValuesInElement(domElement) { + const inputEvent = new Event('input'); + const changeEvent = new Event('change'); + // Reset text inputs + const inputs = document.querySelectorAll("input[type='text'], textarea"); + inputs.forEach(input => { + // Reset input value to its initial stored value + input.value = input.dataset.initialValue; + // Dispatch input event to update any event-driven changes + input.dispatchEvent(inputEvent); + }); + // Reset select elements + let selects = domElement.querySelectorAll("select"); + selects.forEach(select => { + // Reset select value to its initial stored value + select.value = select.dataset.initialValue; + // Dispatch change event to update any event-driven changes + select.dispatchEvent(changeEvent); + }); + } + + /** + * Copies values from the editable row's text inputs into the corresponding + * readonly row cells, formatting them appropriately. + * @param {HTMLElement} editRow - The row containing editable input fields. + * @param {HTMLElement} readOnlyRow - The row where values will be displayed in a non-editable format. + */ + copyEditRowToReadonlyRow(editRow, readOnlyRow) { + let keyTagInput = editRow.querySelector("input[type='text']"); + let selects = editRow.querySelectorAll("select"); + let digestInput = editRow.querySelector("textarea"); + let tds = readOnlyRow.querySelectorAll("td"); + + // Copy the key tag input value + if (keyTagInput) { + tds[0].innerText = keyTagInput.value || ""; + } + + // Copy select values (showing the selected label instead of value) + if (selects[0]) { + let selectedOption = selects[0].options[selects[0].selectedIndex]; + if (tds[1]) { + tds[1].innerHTML = `${selectedOption ? selectedOption.text : ""}`; + } + } + if (selects[1]) { + let selectedOption = selects[1].options[selects[1].selectedIndex]; + if (tds[2]) { + tds[2].innerText = selectedOption ? selectedOption.text : ""; + } + } + + // Copy the digest input value + if (digestInput) { + tds[3].innerHTML = `${digestInput.value || ""}`; + } + } + + /** + * Resets the form change state. + * This method marks the form as unchanged by setting `formChanged` to false. + * It is useful for tracking whether a user has modified any form fields. + */ + resetFormChanged() { + this.formChanged = false; + } + + /** + * Removes all existing alert messages from the main content area. + * This ensures that only the latest alert is displayed to the user. + */ + resetAlerts() { + const mainContent = document.getElementById("main-content"); + if (mainContent) { + // Remove all alert elements within the main content area + mainContent.querySelectorAll(".usa-alert:not(.usa-alert--do-not-reset)").forEach(alert => alert.remove()); + } else { + console.warn("Expecting main-content DOM element"); + } + } + + /** + * Displays an alert message at the top of the main content area. + * It first removes any existing alerts before adding a new one to ensure only the latest alert is visible. + * @param {string} level - The alert level (e.g., 'error', 'success', 'warning', 'info'). + * @param {string} message - The message to display inside the alert. + */ + addAlert(level, message) { + this.resetAlerts(); // Remove any existing alerts before adding a new one + + const mainContent = document.getElementById("main-content"); + if (!mainContent) return; + + // Create a new alert div with appropriate classes based on alert level + const alertDiv = document.createElement("div"); + alertDiv.className = `usa-alert usa-alert--${level} usa-alert--slim margin-bottom-2`; + alertDiv.setAttribute("role", "alert"); // Add the role attribute + + // Create the alert body to hold the message text + const alertBody = document.createElement("div"); + alertBody.className = "usa-alert__body"; + alertBody.textContent = message; + + // Append the alert body to the alert div and insert it at the top of the main content area + alertDiv.appendChild(alertBody); + mainContent.insertBefore(alertDiv, mainContent.firstChild); + + // Scroll the page to make the alert visible to the user + scrollToElement("class", "usa-alert__body"); + } +} + +/** + * Initializes the DSDataForm when the DOM is fully loaded. + */ +export function initFormDSData() { + document.addEventListener('DOMContentLoaded', () => { + if (document.getElementById('dsdata-add-button')) { + const dsDataForm = new DSDataForm(); + dsDataForm.init(); + } + }); +} diff --git a/src/registrar/assets/src/js/getgov/form-helpers.js b/src/registrar/assets/src/js/getgov/form-helpers.js index fabfab98a..6c1d37d57 100644 --- a/src/registrar/assets/src/js/getgov/form-helpers.js +++ b/src/registrar/assets/src/js/getgov/form-helpers.js @@ -38,6 +38,16 @@ export function removeErrorsFromElement(domElement) { domElement.querySelectorAll("input.usa-input--error").forEach(input => { input.classList.remove("usa-input--error"); }); + + // Remove the 'usa-input--error' class from all select elements + domElement.querySelectorAll("select.usa-input--error").forEach(select => { + select.classList.remove("usa-input--error"); + }); + + // Remove the 'usa-input--error' class from all textarea elements + domElement.querySelectorAll("textarea.usa-input--error").forEach(textarea => { + textarea.classList.remove("usa-input--error"); + }); } /** diff --git a/src/registrar/assets/src/js/getgov/form-nameservers.js b/src/registrar/assets/src/js/getgov/form-nameservers.js index 57b868d70..75a35f35b 100644 --- a/src/registrar/assets/src/js/getgov/form-nameservers.js +++ b/src/registrar/assets/src/js/getgov/form-nameservers.js @@ -176,6 +176,15 @@ export class NameserverForm { this.executeCallback(); }); } + const cancel_changes_modal = document.getElementById('cancel-changes-modal'); + if (cancel_changes_modal) { + const submitButton = document.getElementById('cancel-changes-click-button'); + const closeButton = cancel_changes_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } const delete_modal = document.getElementById('delete-modal'); if (delete_modal) { const submitButton = document.getElementById('delete-click-button'); @@ -338,7 +347,18 @@ export class NameserverForm { * @param {Event} event - Click event */ handleCancelAddFormClick(event) { - this.resetAddNameserversForm(); + this.callback = () => { + this.resetAddNameserversForm(); + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } } /** @@ -354,10 +374,21 @@ export class NameserverForm { let cancelButton = event.target; // find the closest table row that contains the cancel button let editRow = cancelButton.closest('tr'); - if (editRow) { - this.resetEditRowAndFormAndCollapseEditRow(editRow); + this.callback = () => { + if (editRow) { + this.resetEditRowAndFormAndCollapseEditRow(editRow); + } else { + console.warn("Expected DOM element but did not find it"); + } + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } } else { - console.warn("Expected DOM element but did not find it"); + this.executeCallback(); } } diff --git a/src/registrar/assets/src/js/getgov/formset-forms.js b/src/registrar/assets/src/js/getgov/formset-forms.js index b4a40e5cf..1d2724b7f 100644 --- a/src/registrar/assets/src/js/getgov/formset-forms.js +++ b/src/registrar/assets/src/js/getgov/formset-forms.js @@ -84,7 +84,7 @@ function markForm(e, formLabel){ } /** - * Prepare the namerservers, DS data and Other Contacts formsets' delete button + * Prepare the Other Contacts formsets' delete button * for the last added form. We call this from the Add function * */ @@ -108,7 +108,7 @@ function prepareNewDeleteButton(btn, formLabel) { } /** - * Prepare the namerservers, DS data and Other Contacts formsets' delete buttons + * Prepare the Other Contacts formsets' delete buttons * We will call this on the forms init * */ @@ -172,16 +172,11 @@ export function initFormsetsForms() { let cloneIndex = 0; let formLabel = ''; let isOtherContactsForm = document.querySelector(".other-contacts-form"); - let isDsDataForm = document.querySelector(".ds-data-form"); let isDotgovDomain = document.querySelector(".dotgov-domain-form"); - if( !(isOtherContactsForm || isDotgovDomain || isDsDataForm) ){ + if( !(isOtherContactsForm || isDotgovDomain) ){ return } - // DNSSEC: DS Data - if (isDsDataForm) { - formLabel = "DS data record"; - // The Other Contacts form - } else if (isOtherContactsForm) { + if (isOtherContactsForm) { formLabel = "Organization contact"; container = document.querySelector("#other-employees"); formIdentifier = "other_contacts" @@ -287,26 +282,3 @@ export function initFormsetsForms() { prepareNewDeleteButton(newDeleteButton, formLabel); } } - -export function triggerModalOnDsDataForm() { - let saveButon = document.querySelector("#save-ds-data"); - - // The view context will cause a hitherto hidden modal trigger to - // show up. On save, we'll test for that modal trigger appearing. We'll - // run that test once every 100 ms for 5 secs, which should balance performance - // while accounting for network or lag issues. - if (saveButon) { - let i = 0; - var tryToTriggerModal = setInterval(function() { - i++; - if (i > 100) { - clearInterval(tryToTriggerModal); - } - let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert"); - if (modalTrigger) { - modalTrigger.click() - clearInterval(tryToTriggerModal); - } - }, 50); - } -} diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js index f077448aa..490874220 100644 --- a/src/registrar/assets/src/js/getgov/main.js +++ b/src/registrar/assets/src/js/getgov/main.js @@ -1,7 +1,8 @@ import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js'; import { initDomainValidators } from './domain-validators.js'; -import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js'; -import { initFormNameservers } from './form-nameservers' +import { initFormsetsForms } from './formset-forms.js'; +import { initFormNameservers } from './form-nameservers'; +import { initFormDSData } from './form-dsdata.js'; import { initializeUrbanizationToggle } from './urbanization.js'; import { userProfileListener, finishUserSetupListener } from './user-profile.js'; import { handleRequestingEntityFieldset } from './requesting-entity.js'; @@ -13,7 +14,6 @@ import { initEditMemberDomainsTable } from './table-edit-member-domains.js'; import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js'; import { initDomainRequestForm } from './domain-request-form.js'; import { initDomainManagersPage } from './domain-managers.js'; -import { initDomainDSData } from './domain-dsdata.js'; import { initDomainDNSSEC } from './domain-dnssec.js'; import { initFormErrorHandling } from './form-errors.js'; import { domain_purpose_choice_callbacks } from './domain-purpose-form.js'; @@ -22,12 +22,14 @@ import { initButtonLinks } from '../getgov-admin/button-utils.js'; initDomainValidators(); initFormsetsForms(); -triggerModalOnDsDataForm(); initFormNameservers(); +initFormDSData(); hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees'); hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null); hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null); +hookupYesNoListener("portfolio_additional_details-working_with_eop", "eop-contact-container", null); +hookupYesNoListener("portfolio_additional_details-has_anything_else_text", 'anything-else-details-container', null); hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container"); hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks); @@ -35,7 +37,6 @@ hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choic hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null); hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null); - initializeUrbanizationToggle(); userProfileListener(); @@ -51,7 +52,6 @@ initEditMemberDomainsTable(); initDomainRequestForm(); initDomainManagersPage(); -initDomainDSData(); initDomainDNSSEC(); initFormErrorHandling(); diff --git a/src/registrar/assets/src/js/getgov/table-base.js b/src/registrar/assets/src/js/getgov/table-base.js index bf561fa1f..54b8db3f1 100644 --- a/src/registrar/assets/src/js/getgov/table-base.js +++ b/src/registrar/assets/src/js/getgov/table-base.js @@ -103,7 +103,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
` @@ -117,7 +117,7 @@ export class DomainRequestsTable extends BaseTable { - ` @@ -56,7 +56,7 @@ export class DomainsTable extends BaseTable { ${markupForSuborganizationRow} -
UIDNameEmail
{escape(domain_manager.username)}' + if not self.is_omb_analyst: + domain_manager_details += f'{escape(domain_manager.username)}' domain_manager_details += f"{escape(full_name)}{escape(domain_manager.email)}
${request.creator ? request.creator : ''} ${request.status} + +
{% endfor %} + {% if perms.registrar.analyst_access_permission or perms.full_access_permission %}
@@ -78,6 +79,7 @@
Analytics
+ {% endif %} {% else %}

{% translate 'You don’t have permission to view or edit anything.' %}

{% endif %} diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 7aa0034b9..2e6f57237 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -11,13 +11,15 @@ {% block field_sets %}
+ {% if not adminform.form.is_omb_analyst %} {# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #} + {% endif %}
- {% if original.state != original.State.DELETED %} + {% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %} Extend expiration date @@ -31,9 +33,11 @@ {% endif %} {% if original.state == original.State.READY or original.state == original.State.ON_HOLD %} + {% if not adminform.form.is_omb_analyst %} | {% endif %} - {% if original.state != original.State.DELETED %} + {% endif %} + {% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %} Remove from registry diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 0baabac17..b3cdeb875 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -6,7 +6,11 @@ {% if show_formatted_name %} {% if user.get_formatted_name %} - {{ user.get_formatted_name }} + {% if adminform.form.show_contact_as_plain_text %} + {{ user.get_formatted_name }} + {% else %} + {{ user.get_formatted_name }} + {% endif %} {% else %} None {% endif %} diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index f12bd67f9..dcf393d82 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -69,7 +69,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% elif field.field.name == "portfolio_senior_official" %}
{% if original_object.portfolio.senior_official %} - {{ field.contents }} + {% if adminform.form.show_contact_as_plain_text %} + {{ field.contents|striptags }} + {% else %} + {{ field.contents }} + {% endif %} {% else %} No senior official found.
{% endif %} @@ -78,7 +82,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% if all_contacts.count > 2 %}
{% for contact in all_contacts %} - {{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %} + {% if adminform.form.show_contact_as_plain_text %} + {{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %} + {% else %} + {{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %} + {% endif %} {% endfor %}
{% else %} @@ -153,6 +161,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)

No additional members found.

{% endif %}
+ {% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %} +
{{ field.contents|striptags }}
+ {% elif field.field.name == "senior_official" and adminform.form.show_contact_as_plain_text %} +
{{ field.contents|striptags }}
{% else %}
{{ field.contents }}
{% endif %} diff --git a/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html b/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html index 7add74323..574c05738 100644 --- a/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html +++ b/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html @@ -16,7 +16,11 @@ {% for admin in admins %} {% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %} - {{ admin.user.get_formatted_name}} + {% if adminform.form.is_omb_analyst %} + {{ admin.user.get_formatted_name }} + {% else %} + {{ admin.user.get_formatted_name}} + {% endif %} {{ admin.user.title }} {% if admin.user.email %} diff --git a/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html b/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html index 87b56cb60..54ac502d1 100644 --- a/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html +++ b/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html @@ -30,6 +30,9 @@ No senior official found. Create one now.
{% endif %} + + {% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %} +
{{ field.contents|striptags }}
{% else %}
{{ field.contents }}
{% endif %} diff --git a/src/registrar/templates/django/forms/widgets/textarea.html b/src/registrar/templates/django/forms/widgets/textarea.html index 068f15399..ffe5dcd4a 100644 --- a/src/registrar/templates/django/forms/widgets/textarea.html +++ b/src/registrar/templates/django/forms/widgets/textarea.html @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/src/registrar/templates/domain_base.html b/src/registrar/templates/domain_base.html index 60c931dce..5637fa52b 100644 --- a/src/registrar/templates/domain_base.html +++ b/src/registrar/templates/domain_base.html @@ -9,7 +9,7 @@
-
+

@@ -21,7 +21,7 @@ {% endif %}

-
+
{% if not domain.domain_info %}
diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 95e8e3d5f..ceab937bc 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -34,122 +34,376 @@ {% endif %} {% endblock breadcrumb %} - {% if domain.dnssecdata is None %} -
-
- You have no DS data added. Enable DNSSEC by adding DS data. +
+
+

DS data

+
+ +
+
+ +

In order to enable DNSSEC, you must first configure it with your DNS provider.

+ +

Click β€œAdd DS data” and enter the values given by your DNS provider for DS (Delegation Signer) data. You can add a maximum of 8 DS records.

+ + {% comment %} + This template supports the rendering of three different DS data forms, conditionally displayed: + 1 - Add DS Data form (rendered when there are no existing DS data records defined for the domain) + 2 - DS Data table (rendered when the domain has existing DS data, which can be viewed and edited) + 3 - Add DS Data form (rendered above the DS Data table to add a single additional DS Data record) + {% endcomment %} + + {% if formset.initial and formset.forms.0.initial %} + + {% comment %}This section renders both the DS Data table and the Add DS Data form {% endcomment %} + + {% include "includes/required_fields.html" %} + +
+ {% csrf_token %} + {{ formset.management_form }} + + {% for form in formset %} + {% if forloop.last and not form.initial %} + + {% comment %} + This section renders the Add DS data form. + This section does not render if the last form has initial data (this occurs if 8 DS data records already exist) + {% endcomment %} + + + {% endif %} + {% endfor %} + +
+ + + + + + + + + + + + + {% for form in formset %} + {% if not forloop.last or form.initial %} + + {% comment %} + This section renders table rows for each existing DS data records. Two rows are rendered, a readonly row + and an edit row. Only one of which is displayed at a time. + {% endcomment %} + + + + + + + + + + + + + + + + + + {% endif %} + {% endfor %} + +
Your DS data records
Key tagAlgorithmDigest typeDigestAction
{{ form.key_tag.value }} + + {% for value, label in form.algorithm.field.choices %} + {% if value|stringformat:"s" == form.algorithm.value|stringformat:"s" %} + {{ label }} + {% endif %} + {% endfor %} + + + {% for value, label in form.digest_type.field.choices %} + {% if value|stringformat:"s" == form.digest_type.value|stringformat:"s" %} + {{ label }} + {% endif %} + {% endfor %} + + {{ form.digest.value }} + +
+ + + + + Delete + + +
+
+ +
+ +
+
+
+
+
+ + {% else %} + + {% comment %} + This section renders Add DS Data form which renders when there are no existing + DS records defined on the domain. + {% endcomment %} + + {% endif %} -

DS data

- -

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

- -

Enter the values given by your DNS provider for DS data.

- - {% include "includes/required_fields.html" %} - -
- {% csrf_token %} - {{ formset.management_form }} - - {% for form in formset %} -
- - DS data record {{forloop.counter}} - -

DS data record {{forloop.counter}}

- -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.key_tag %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.algorithm %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest_type %} - {% endwith %} -
-
- -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest %} - {% endwith %} -
-
- -
-
- -
-
- -
- {% endfor %} - - - - - - -
- - {% if trigger_modal %} +
+ {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_id="unsaved-changes-click-button" modal_button_text="Continue without saving" cancel_button_text="Go back" %} +
+ + +
+ {% include 'includes/modal.html' with modal_heading="Are you sure you want to cancel your changes?" modal_description="This action cannot be undone." modal_button_id="cancel-changes-click-button" modal_button_text="Yes, cancel" cancel_button_text="Go back" %} +
+ + +
+ {% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this DS data record?" modal_description="This action cannot be undone." modal_button_id="delete-click-button" modal_button_text="Yes, delete" modal_button_class="usa-button--secondary" %} +
+ + - {% endif %} - {# Use data-force-action to take esc out of the equation and pass cancel_button_resets_ds_form to effectuate a reset in the view #}
- {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-override-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %} + {% include 'includes/modal.html' with modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-dnssec-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %}
-
- {% csrf_token %} - -
-
- {% csrf_token %} - -
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index bd8216d05..acd73cc3b 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -82,7 +82,7 @@ This section does not render if the last form has initial data (this occurs if 13 nameservers already exist) {% endcomment %} -