mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-04 08:52:16 +02:00
Merge branch 'main' into litterbox/3418-refactor-create-federal-portfolio
This commit is contained in:
commit
6808b4869a
118 changed files with 6488 additions and 1521 deletions
76
.github/ISSUE_TEMPLATE/design-issue.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/design-issue.yml
vendored
Normal file
|
@ -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
|
||||
<br>
|
||||
- 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
|
||||
<br>
|
||||
- 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.
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
2
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
292
src/package-lock.json
generated
292
src/package-lock.json
generated
|
@ -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": {
|
||||
|
|
File diff suppressed because it is too large
Load diff
60
src/registrar/assets/src/js/getgov-admin/andi.js
Normal file
60
src/registrar/assets/src/js/getgov-admin/andi.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
This function intercepts all select2 dropdowns and adds aria content.
|
||||
It relies on an override in detail_table_fieldset.html that provides
|
||||
a span with a corresponding id for aria-describedby content.
|
||||
|
||||
This allows us to avoid overriding aria-label, which is used by select2
|
||||
to send the current dropdown selection to ANDI.
|
||||
*/
|
||||
export function initAriaInjectionsForSelect2Dropdowns() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Find all spans with "--aria-description" in their id
|
||||
const descriptionSpans = document.querySelectorAll('span[id*="--aria-description"]');
|
||||
|
||||
descriptionSpans.forEach(function (span) {
|
||||
// Extract the base ID from the span's id (remove "--aria-description")
|
||||
const fieldId = span.id.replace('--aria-description', '');
|
||||
const field = document.getElementById(fieldId);
|
||||
|
||||
if (field) {
|
||||
// If Select2 is already initialized, apply aria-describedby immediately
|
||||
if (field.classList.contains('select2-hidden-accessible')) {
|
||||
applyAriaDescribedBy(field, span.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use MutationObserver to detect Select2 initialization
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
if (document.getElementById(fieldId)?.classList.contains("select2-hidden-accessible")) {
|
||||
applyAriaDescribedBy(field, span.id);
|
||||
observer.disconnect(); // Stop observing after applying attributes
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to apply aria-describedby to Select2 UI
|
||||
function applyAriaDescribedBy(field, descriptionId) {
|
||||
let select2ElementDetected = false;
|
||||
const select2Id = "select2-" + field.id + "-container";
|
||||
|
||||
// Find the Select2 selection box
|
||||
const select2SpanThatTriggersAria = document.querySelector(`span[aria-labelledby='${select2Id}']`);
|
||||
|
||||
if (select2SpanThatTriggersAria) {
|
||||
select2SpanThatTriggersAria.setAttribute('aria-describedby', descriptionId);
|
||||
select2ElementDetected = true;
|
||||
}
|
||||
|
||||
// If no Select2 component was detected, apply aria-describedby directly to the field
|
||||
if (!select2ElementDetected) {
|
||||
field.setAttribute('aria-describedby', descriptionId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -14,10 +14,12 @@ import {
|
|||
initFilterFocusListeners } from './domain-request-form.js';
|
||||
import { initDomainFormTargetBlankButtons } from './domain-form.js';
|
||||
import { initDynamicPortfolioFields } from './portfolio-form.js';
|
||||
import { initDynamicPortfolioPermissionFields } from './portfolio-permissions-form.js'
|
||||
import { initDynamicDomainInformationFields } from './domain-information-form.js';
|
||||
import { initDynamicDomainFields } from './domain-form.js';
|
||||
import { initAnalyticsDashboard } from './analytics.js';
|
||||
import { initButtonLinks } from './button-utils.js';
|
||||
import { initAriaInjectionsForSelect2Dropdowns } from './andi.js'
|
||||
|
||||
// General
|
||||
initModals();
|
||||
|
@ -25,6 +27,7 @@ initCopyToClipboard();
|
|||
initFilterHorizontalWidget();
|
||||
initDescriptions();
|
||||
initSubmitBar();
|
||||
initAriaInjectionsForSelect2Dropdowns();
|
||||
initButtonLinks();
|
||||
|
||||
// Domain request
|
||||
|
@ -44,6 +47,9 @@ initDynamicDomainFields();
|
|||
// Portfolio
|
||||
initDynamicPortfolioFields();
|
||||
|
||||
// Portfolio permissions
|
||||
initDynamicPortfolioPermissionFields();
|
||||
|
||||
// Domain information
|
||||
initDynamicDomainInformationFields();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { hideElement, showElement } from './helpers-admin.js';
|
||||
|
||||
/**
|
||||
* A function for dynamically changing fields on the UserPortfolioPermissions
|
||||
* and PortfolioInvitation admin forms
|
||||
*/
|
||||
function handlePortfolioPermissionFields(){
|
||||
|
||||
const roleDropdown = document.getElementById("id_role");
|
||||
const domainPermissionsField = document.querySelector(".field-domain_permissions");
|
||||
const domainRequestPermissionsField = document.querySelector(".field-request_permissions");
|
||||
const memberPermissionsField = document.querySelector(".field-member_permissions");
|
||||
|
||||
/**
|
||||
* Updates the visibility of portfolio permissions fields based on the selected role.
|
||||
*
|
||||
* This function checks the value of the role dropdown (`roleDropdown`):
|
||||
* - If the selected role is "organization_member":
|
||||
* - Shows the domain permissions field (`domainPermissionsField`).
|
||||
* - Shows the domain request permissions field (`domainRequestPermissionsField`).
|
||||
* - Shows the member permissions field (`memberPermissionsField`).
|
||||
* - Otherwise:
|
||||
* - Hides all the above fields.
|
||||
*
|
||||
* The function ensures that the appropriate fields are dynamically displayed
|
||||
* or hidden depending on the role selection in the form.
|
||||
*/
|
||||
function updatePortfolioPermissionsFormVisibility() {
|
||||
if (roleDropdown && domainPermissionsField && domainRequestPermissionsField && memberPermissionsField) {
|
||||
if (roleDropdown.value === "organization_member") {
|
||||
showElement(domainPermissionsField);
|
||||
showElement(domainRequestPermissionsField);
|
||||
showElement(memberPermissionsField);
|
||||
} else {
|
||||
hideElement(domainPermissionsField);
|
||||
hideElement(domainRequestPermissionsField);
|
||||
hideElement(memberPermissionsField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets event listeners for key UI elements.
|
||||
*/
|
||||
function setEventListeners() {
|
||||
if (roleDropdown) {
|
||||
roleDropdown.addEventListener("change", function() {
|
||||
updatePortfolioPermissionsFormVisibility();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Run initial setup functions
|
||||
updatePortfolioPermissionsFormVisibility();
|
||||
setEventListeners();
|
||||
}
|
||||
|
||||
export function initDynamicPortfolioPermissionFields() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let isPortfolioPermissionPage = document.getElementById("userportfoliopermission_form");
|
||||
let isPortfolioInvitationPage = document.getElementById("portfolioinvitation_form")
|
||||
if (isPortfolioPermissionPage || isPortfolioInvitationPage) {
|
||||
handlePortfolioPermissionFields();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainDNSSEC() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainDSData() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainManagersPage() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
41
src/registrar/assets/src/js/getgov/domain-purpose-form.js
Normal file
41
src/registrar/assets/src/js/getgov/domain-purpose-form.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { showElement } from './helpers.js';
|
||||
|
||||
export const domain_purpose_choice_callbacks = {
|
||||
'new': {
|
||||
callback: function(value, element) {
|
||||
//show the purpose details container
|
||||
showElement(element);
|
||||
// change just the text inside the em tag
|
||||
const labelElement = element.querySelector('.usa-label em');
|
||||
labelElement.innerHTML = 'Explain why a new domain is required and why a ' +
|
||||
'subdomain of an existing domain doesn\'t meet your needs.' +
|
||||
'<br><br>' + // Adding double line break for spacing
|
||||
'Include any data that supports a clear public benefit or ' +
|
||||
'evidence user need for this new domain. ' +
|
||||
'<span class="usa-label--required">*</span>';
|
||||
},
|
||||
element: document.getElementById('purpose-details-container')
|
||||
},
|
||||
'redirect': {
|
||||
callback: function(value, element) {
|
||||
// show the purpose details container
|
||||
showElement(element);
|
||||
// change just the text inside the em tag
|
||||
const labelElement = element.querySelector('.usa-label em');
|
||||
labelElement.innerHTML = 'Explain why a redirect is necessary. ' +
|
||||
'<span class="usa-label--required">*</span>';
|
||||
},
|
||||
element: document.getElementById('purpose-details-container')
|
||||
},
|
||||
'other': {
|
||||
callback: function(value, element) {
|
||||
// Show the purpose details container
|
||||
showElement(element);
|
||||
// change just the text inside the em tag
|
||||
const labelElement = element.querySelector('.usa-label em');
|
||||
labelElement.innerHTML = 'Describe how this domain will be used. ' +
|
||||
'<span class="usa-label--required">*</span>';
|
||||
},
|
||||
element: document.getElementById('purpose-details-container')
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainRequestForm() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
57
src/registrar/assets/src/js/getgov/form-helpers.js
Normal file
57
src/registrar/assets/src/js/getgov/form-helpers.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Helper function to submit a form
|
||||
* @param {} form_id - the id of the form to be submitted
|
||||
*/
|
||||
export function submitForm(form_id) {
|
||||
let form = document.getElementById(form_id);
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else {
|
||||
console.error("Form '" + form_id + "' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all error-related classes and messages from the specified DOM element.
|
||||
* This method cleans up validation errors by removing error highlighting from input fields,
|
||||
* labels, and form groups, as well as deleting error message elements.
|
||||
* @param {HTMLElement} domElement - The parent element within which errors should be cleared.
|
||||
*/
|
||||
export function removeErrorsFromElement(domElement) {
|
||||
// Remove the 'usa-form-group--error' class from all div elements
|
||||
domElement.querySelectorAll("div.usa-form-group--error").forEach(div => {
|
||||
div.classList.remove("usa-form-group--error");
|
||||
});
|
||||
|
||||
// Remove the 'usa-label--error' class from all label elements
|
||||
domElement.querySelectorAll("label.usa-label--error").forEach(label => {
|
||||
label.classList.remove("usa-label--error");
|
||||
});
|
||||
|
||||
// Remove all error message divs whose ID ends with '__error-message'
|
||||
domElement.querySelectorAll("div[id$='__error-message']").forEach(errorDiv => {
|
||||
errorDiv.remove();
|
||||
});
|
||||
|
||||
// Remove the 'usa-input--error' class from all input elements
|
||||
domElement.querySelectorAll("input.usa-input--error").forEach(input => {
|
||||
input.classList.remove("usa-input--error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all form-level error messages displayed in the UI.
|
||||
* The form error messages are contained within div elements with the ID 'form-errors'.
|
||||
* Since multiple elements with the same ID may exist (even though not syntactically correct),
|
||||
* this function removes them iteratively.
|
||||
*/
|
||||
export function removeFormErrors() {
|
||||
let formErrorDiv = document.getElementById("form-errors");
|
||||
|
||||
// Recursively remove all instances of form error divs
|
||||
while (formErrorDiv) {
|
||||
formErrorDiv.remove();
|
||||
formErrorDiv = document.getElementById("form-errors");
|
||||
}
|
||||
}
|
516
src/registrar/assets/src/js/getgov/form-nameservers.js
Normal file
516
src/registrar/assets/src/js/getgov/form-nameservers.js
Normal file
|
@ -0,0 +1,516 @@
|
|||
import { showElement, hideElement, scrollToElement } from './helpers';
|
||||
import { removeErrorsFromElement, removeFormErrors } from './form-helpers';
|
||||
|
||||
export class NameserverForm {
|
||||
constructor() {
|
||||
this.addNameserverButton = document.getElementById('nameserver-add-button');
|
||||
this.addNameserversForm = document.querySelector('.add-nameservers-form');
|
||||
this.domain = '';
|
||||
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 NameserverForm by setting up display and event listeners.
|
||||
*/
|
||||
init() {
|
||||
this.initializeNameserverFormDisplay();
|
||||
this.initializeEventListeners();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines the initial display state of the nameserver form,
|
||||
* handling validation errors and setting visibility of elements accordingly.
|
||||
*/
|
||||
initializeNameserverFormDisplay() {
|
||||
|
||||
const domainName = document.getElementById('id_form-0-domain');
|
||||
if (domainName) {
|
||||
this.domain = domainName.value;
|
||||
} else {
|
||||
console.warn("Form expects a dom element, id_form-0-domain");
|
||||
}
|
||||
|
||||
// Check if exactly two nameserver forms exist: id_form-1-server is present but id_form-2-server is not
|
||||
const secondNameserver = document.getElementById('id_form-1-server');
|
||||
const thirdNameserver = document.getElementById('id_form-2-server'); // This should not exist
|
||||
|
||||
// Check if there are error messages in the form (indicated by elements with class 'usa-alert--error')
|
||||
const errorMessages = document.querySelectorAll('.usa-alert--error');
|
||||
|
||||
// This check indicates that there are exactly two forms (which is the case for the Add New Nameservers form)
|
||||
// and there is at least one error in the form. In this case, show the Add New Nameservers form, and
|
||||
// indicate that the form has changed
|
||||
if (this.addNameserversForm && secondNameserver && !thirdNameserver && errorMessages.length > 0) {
|
||||
showElement(this.addNameserversForm);
|
||||
this.formChanged = true;
|
||||
}
|
||||
|
||||
// This check indicates that there is either an Add New Nameservers form or an Add New Nameserver form
|
||||
// and that form has errors in it. In this case, show the form, and indicate that the form has
|
||||
// changed.
|
||||
if (this.addNameserversForm && this.addNameserversForm.querySelector('.usa-input--error')) {
|
||||
showElement(this.addNameserversForm);
|
||||
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('nameserver-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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// hide ip in forms unless nameserver ends with domain name
|
||||
let formIndex = 0;
|
||||
while (document.getElementById('id_form-' + formIndex + '-domain')) {
|
||||
let serverInput = document.getElementById('id_form-' + formIndex + '-server');
|
||||
let ipInput = document.getElementById('id_form-' + formIndex + '-ip');
|
||||
if (serverInput && ipInput) {
|
||||
let serverValue = serverInput.value.trim(); // Get the value and trim spaces
|
||||
let ipParent = ipInput.parentElement; // Get the parent element of ipInput
|
||||
|
||||
if (ipParent && !serverValue.endsWith('.' + this.domain)) {
|
||||
hideElement(ipParent); // Hide the parent element of ipInput
|
||||
}
|
||||
}
|
||||
formIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches event listeners to relevant UI elements for interaction handling.
|
||||
*/
|
||||
initializeEventListeners() {
|
||||
this.addNameserverButton.addEventListener('click', this.handleAddFormClick);
|
||||
|
||||
const editButtons = document.querySelectorAll('.nameserver-edit');
|
||||
editButtons.forEach(editButton => {
|
||||
editButton.addEventListener('click', this.handleEditClick);
|
||||
});
|
||||
|
||||
const cancelButtons = document.querySelectorAll('.nameserver-cancel');
|
||||
cancelButtons.forEach(cancelButton => {
|
||||
cancelButton.addEventListener('click', this.handleCancelClick);
|
||||
});
|
||||
|
||||
const cancelAddFormButtons = document.querySelectorAll('.nameserver-cancel-add-form');
|
||||
cancelAddFormButtons.forEach(cancelAddFormButton => {
|
||||
cancelAddFormButton.addEventListener('click', this.handleCancelAddFormClick);
|
||||
});
|
||||
|
||||
const deleteButtons = document.querySelectorAll('.nameserver-delete');
|
||||
deleteButtons.forEach(deleteButton => {
|
||||
deleteButton.addEventListener('click', this.handleDeleteClick);
|
||||
});
|
||||
|
||||
const deleteKebabButtons = document.querySelectorAll('.nameserver-delete-kebab');
|
||||
deleteKebabButtons.forEach(deleteKebabButton => {
|
||||
deleteKebabButton.addEventListener('click', this.handleDeleteKebabClick);
|
||||
});
|
||||
|
||||
const textInputs = document.querySelectorAll("input[type='text']");
|
||||
textInputs.forEach(input => {
|
||||
input.addEventListener("input", () => {
|
||||
this.formChanged = true;
|
||||
});
|
||||
});
|
||||
|
||||
// Add a specific listener for 'id_form-{number}-server' inputs to make
|
||||
// nameserver forms 'smart'. Inputs on server field will change the
|
||||
// display value of the associated IP address field.
|
||||
let formIndex = 0;
|
||||
while (document.getElementById(`id_form-${formIndex}-server`)) {
|
||||
let serverInput = document.getElementById(`id_form-${formIndex}-server`);
|
||||
let ipInput = document.getElementById(`id_form-${formIndex}-ip`);
|
||||
if (serverInput && ipInput) {
|
||||
let ipParent = ipInput.parentElement; // Get the parent element of ipInput
|
||||
let ipTd = ipParent.parentElement;
|
||||
// add an event listener on the server input that adjusts visibility
|
||||
// and value of the ip input (and its parent)
|
||||
serverInput.addEventListener("input", () => {
|
||||
let serverValue = serverInput.value.trim();
|
||||
if (ipParent && ipTd) {
|
||||
if (serverValue.endsWith('.' + this.domain)) {
|
||||
showElement(ipParent); // Show IP field if the condition matches
|
||||
ipTd.classList.add('width-40p');
|
||||
} else {
|
||||
hideElement(ipParent); // Hide IP field otherwise
|
||||
ipTd.classList.remove('width-40p');
|
||||
ipInput.value = ""; // Set the IP value to blank
|
||||
}
|
||||
} else {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
}
|
||||
});
|
||||
}
|
||||
formIndex++; // Move to the next index
|
||||
}
|
||||
|
||||
// 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 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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Nameserver' 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.addNameserversForm) {
|
||||
// Check if this.addNameserversForm is visible (i.e., does not have 'display-none')
|
||||
if (!this.addNameserversForm.classList.contains('display-none')) {
|
||||
this.resetAddNameserversForm();
|
||||
}
|
||||
// show nameservers form
|
||||
showElement(this.addNameserversForm);
|
||||
} else {
|
||||
this.addAlert("error", "You’ve reached the maximum amount of name server records (13). 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.addNameserversForm is visible (i.e., does not have 'display-none')
|
||||
if (this.addNameserversForm && !this.addNameserversForm.classList.contains('display-none')) {
|
||||
this.resetAddNameserversForm();
|
||||
}
|
||||
// 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 nameserver
|
||||
* after displaying modal and performing check for minimum number of nameservers.
|
||||
* @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 nameserver
|
||||
* after displaying modal and performing check for minimum number of nameservers.
|
||||
* @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 nameserver row after verifying the minimum required nameservers exist.
|
||||
* If there are only two nameservers left, deletion is prevented, and an alert is shown.
|
||||
* If deletion proceeds, the input fields are cleared, and the form is submitted.
|
||||
* @param {HTMLElement} editRow - The row corresponding to the nameserver being deleted.
|
||||
*/
|
||||
deleteRow(editRow) {
|
||||
// Check if at least two nameserver forms exist
|
||||
const fourthNameserver = document.getElementById('id_form-3-server'); // This should exist
|
||||
// This checks that at least 3 nameservers exist prior to the delete of a row, and if not
|
||||
// display an error alert
|
||||
if (fourthNameserver) {
|
||||
this.callback = () => {
|
||||
hideElement(editRow);
|
||||
let textInputs = editRow.querySelectorAll("input[type='text']");
|
||||
textInputs.forEach(input => {
|
||||
input.value = "";
|
||||
});
|
||||
document.querySelector("form").submit();
|
||||
};
|
||||
let modalTrigger = document.querySelector('#delete_trigger');
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
this.addAlert("error", "At least two name servers are required. To proceed, add a new name server before removing this name server. If you need help, email us at help@get.gov.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event on the "Cancel" button in the add nameserver form.
|
||||
* Resets the form fields and hides the add form section.
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleCancelAddFormClick(event) {
|
||||
this.resetAddNameserversForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
if (editRow) {
|
||||
this.resetEditRowAndFormAndCollapseEditRow(editRow);
|
||||
} else {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Nameserver' form by clearing its input fields, removing errors,
|
||||
* and hiding the form to return it to its initial state.
|
||||
*/
|
||||
resetAddNameserversForm() {
|
||||
if (this.addNameserversForm) {
|
||||
// reset the values set in addNameserversForm
|
||||
this.resetInputValuesInElement(this.addNameserversForm);
|
||||
// remove errors from the addNameserversForm
|
||||
removeErrorsFromElement(this.addNameserversForm);
|
||||
// remove errors from the entire form
|
||||
removeFormErrors();
|
||||
// reset formChanged
|
||||
this.resetFormChanged();
|
||||
// hide the addNameserversForm
|
||||
hideElement(this.addNameserversForm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
let textInputs = domElement.querySelectorAll("input[type='text']");
|
||||
textInputs.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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 textInputs = editRow.querySelectorAll("input[type='text']");
|
||||
let tds = readOnlyRow.querySelectorAll("td");
|
||||
let updatedText = '';
|
||||
|
||||
// If a server name exists, store its value
|
||||
if (textInputs[0]) {
|
||||
updatedText = textInputs[0].value;
|
||||
}
|
||||
|
||||
// If an IP address exists, append it in parentheses next to the server name
|
||||
if (textInputs[1] && textInputs[1].value) {
|
||||
updatedText = updatedText + " (" + textInputs[1].value + ")";
|
||||
}
|
||||
|
||||
// Assign the formatted text to the first column of the readonly row
|
||||
if (tds[0]) {
|
||||
tds[0].innerText = updatedText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 NameserverForm when the DOM is fully loaded.
|
||||
*/
|
||||
export function initFormNameservers() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.getElementById('nameserver-add-button')) {
|
||||
const nameserverForm = new NameserverForm();
|
||||
nameserverForm.init();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* We will call this on the forms init, and also every time we add a form
|
||||
*
|
||||
*/
|
||||
function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
||||
function removeForm(e, formLabel, addButton, formIdentifier){
|
||||
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
|
||||
let formToRemove = e.target.closest(".repeatable-form");
|
||||
formToRemove.remove();
|
||||
|
@ -38,48 +38,7 @@ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
|||
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
||||
}
|
||||
|
||||
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
|
||||
// inject the USWDS required markup and make sure the INPUT is required
|
||||
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
|
||||
|
||||
// Remove the word optional
|
||||
innerSpan.textContent = innerSpan.textContent.replace(/\s*\(\s*optional\s*\)\s*/, '');
|
||||
|
||||
// Create a new element
|
||||
const newElement = document.createElement('abbr');
|
||||
newElement.textContent = '*';
|
||||
newElement.setAttribute("title", "required");
|
||||
newElement.classList.add("usa-hint", "usa-hint--required");
|
||||
|
||||
// Append the new element to the label
|
||||
node.appendChild(newElement);
|
||||
// Find the next sibling that is an input element
|
||||
let nextInputElement = node.nextElementSibling;
|
||||
|
||||
while (nextInputElement) {
|
||||
if (nextInputElement.tagName === 'INPUT') {
|
||||
// Found the next input element
|
||||
nextInputElement.setAttribute("required", "")
|
||||
break;
|
||||
}
|
||||
nextInputElement = nextInputElement.nextElementSibling;
|
||||
}
|
||||
nextInputElement.required = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Display the add more button if we have less than 13 forms
|
||||
if (isNameserversForm && forms.length <= 13) {
|
||||
addButton.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
if (isNameserversForm && forms.length < 3) {
|
||||
// Hide the delete buttons on the remaining nameservers
|
||||
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||
deleteButton.setAttribute("disabled", "true");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -131,7 +90,6 @@ function markForm(e, formLabel){
|
|||
*/
|
||||
function prepareNewDeleteButton(btn, formLabel) {
|
||||
let formIdentifier = "form"
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let addButton = document.querySelector("#add-form");
|
||||
|
||||
|
@ -144,7 +102,7 @@ function prepareNewDeleteButton(btn, formLabel) {
|
|||
} else {
|
||||
// We will remove the forms and re-order the formset
|
||||
btn.addEventListener('click', function(e) {
|
||||
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
|
||||
removeForm(e, formLabel, addButton, formIdentifier);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +115,6 @@ function prepareNewDeleteButton(btn, formLabel) {
|
|||
function prepareDeleteButtons(formLabel) {
|
||||
let formIdentifier = "form"
|
||||
let deleteButtons = document.querySelectorAll(".delete-record");
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let addButton = document.querySelector("#add-form");
|
||||
if (isOtherContactsForm) {
|
||||
|
@ -174,7 +131,7 @@ function prepareDeleteButtons(formLabel) {
|
|||
} else {
|
||||
// We will remove the forms and re-order the formset
|
||||
deleteButton.addEventListener('click', function(e) {
|
||||
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
|
||||
removeForm(e, formLabel, addButton, formIdentifier);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -214,16 +171,14 @@ export function initFormsetsForms() {
|
|||
let addButton = document.querySelector("#add-form");
|
||||
let cloneIndex = 0;
|
||||
let formLabel = '';
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let isDsDataForm = document.querySelector(".ds-data-form");
|
||||
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
|
||||
// The Nameservers formset features 2 required and 11 optionals
|
||||
if (isNameserversForm) {
|
||||
// cloneIndex = 2;
|
||||
formLabel = "Name server";
|
||||
if( !(isOtherContactsForm || isDotgovDomain || isDsDataForm) ){
|
||||
return
|
||||
}
|
||||
// DNSSEC: DS Data
|
||||
} else if (isDsDataForm) {
|
||||
if (isDsDataForm) {
|
||||
formLabel = "DS data record";
|
||||
// The Other Contacts form
|
||||
} else if (isOtherContactsForm) {
|
||||
|
@ -235,11 +190,6 @@ export function initFormsetsForms() {
|
|||
}
|
||||
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
|
||||
|
||||
// On load: Disable the add more button if we have 13 forms
|
||||
if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) {
|
||||
addButton.setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
// Hide forms which have previously been deleted
|
||||
hideDeletedForms()
|
||||
|
||||
|
@ -258,33 +208,6 @@ export function initFormsetsForms() {
|
|||
// For the eample on Nameservers
|
||||
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
|
||||
|
||||
// Some Nameserver form checks since the delete can mess up the source object we're copying
|
||||
// in regards to required fields and hidden delete buttons
|
||||
if (isNameserversForm) {
|
||||
|
||||
// If the source element we're copying has required on an input,
|
||||
// reset that input
|
||||
let formRequiredNeedsCleanUp = newForm.innerHTML.includes('*');
|
||||
if (formRequiredNeedsCleanUp) {
|
||||
newForm.querySelector('label abbr').remove();
|
||||
// Get all input elements within the container
|
||||
const inputElements = newForm.querySelectorAll("input");
|
||||
// Loop through each input element and remove the 'required' attribute
|
||||
inputElements.forEach((input) => {
|
||||
if (input.hasAttribute("required")) {
|
||||
input.removeAttribute("required");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the source element we're copying has an disabled delete button,
|
||||
// enable that button
|
||||
let deleteButton= newForm.querySelector('.delete-record');
|
||||
if (deleteButton.hasAttribute("disabled")) {
|
||||
deleteButton.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
formNum++;
|
||||
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`);
|
||||
|
@ -305,14 +228,7 @@ export function initFormsetsForms() {
|
|||
deleteButton.setAttribute("aria-labelledby", header.id);
|
||||
deleteButton.setAttribute("aria-describedby", deleteDescription.id);
|
||||
} else {
|
||||
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
|
||||
// if indices 0 or 1 have been deleted
|
||||
let containsOptional = newForm.innerHTML.includes('(optional)');
|
||||
if (isNameserversForm && !containsOptional) {
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum} (optional)`);
|
||||
} else {
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
||||
}
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
||||
}
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
||||
newForm.innerHTML = newForm.innerHTML.replace(/\n/g, ''); // Remove newline characters
|
||||
|
@ -369,20 +285,6 @@ export function initFormsetsForms() {
|
|||
let newDeleteButton = newForm.querySelector(".delete-record");
|
||||
if (newDeleteButton)
|
||||
prepareNewDeleteButton(newDeleteButton, formLabel);
|
||||
|
||||
// Disable the add more button if we have 13 forms
|
||||
if (isNameserversForm && formNum == 13) {
|
||||
addButton.setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
if (isNameserversForm && forms.length >= 2) {
|
||||
// Enable the delete buttons on the nameservers
|
||||
forms.forEach((form, index) => {
|
||||
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||
deleteButton.removeAttribute("disabled");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -408,22 +310,3 @@ export function triggerModalOnDsDataForm() {
|
|||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the delete buttons on nameserver forms on page load if < 3 forms
|
||||
*
|
||||
*/
|
||||
export function nameserversFormListener() {
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
if (isNameserversForm) {
|
||||
let forms = document.querySelectorAll(".repeatable-form");
|
||||
if (forms.length < 3) {
|
||||
// Hide the delete buttons on the 2 nameservers
|
||||
forms.forEach((form) => {
|
||||
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||
deleteButton.setAttribute("disabled", "true");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,19 +84,6 @@ export function getCsrfToken() {
|
|||
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to submit a form
|
||||
* @param {} form_id - the id of the form to be submitted
|
||||
*/
|
||||
export function submitForm(form_id) {
|
||||
let form = document.getElementById(form_id);
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else {
|
||||
console.error("Form '" + form_id + "' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to strip HTML tags
|
||||
* THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { hookupYesNoListener, hookupRadioTogglerListener } from './radios.js';
|
||||
import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js';
|
||||
import { initDomainValidators } from './domain-validators.js';
|
||||
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
||||
import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js';
|
||||
import { initFormNameservers } from './form-nameservers'
|
||||
import { initializeUrbanizationToggle } from './urbanization.js';
|
||||
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
||||
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
||||
|
@ -15,17 +16,26 @@ 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';
|
||||
import { initButtonLinks } from '../getgov-admin/button-utils.js';
|
||||
|
||||
initDomainValidators();
|
||||
|
||||
initFormsetsForms();
|
||||
triggerModalOnDsDataForm();
|
||||
nameserversFormListener();
|
||||
initFormNameservers();
|
||||
|
||||
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("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
|
||||
|
||||
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
|
||||
|
||||
hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
|
||||
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
|
||||
|
||||
|
||||
initializeUrbanizationToggle();
|
||||
|
||||
userProfileListener();
|
||||
|
|
|
@ -17,7 +17,7 @@ export function hookupYesNoListener(radioButtonName, elementIdToShowIfYes, eleme
|
|||
'False': elementIdToShowIfNo
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hookup listeners for radio togglers in form fields.
|
||||
*
|
||||
|
@ -75,3 +75,57 @@ export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
|
|||
handleRadioButtonChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookup listeners for radio togglers in form fields.
|
||||
*
|
||||
* Parameters:
|
||||
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
|
||||
* - valueToCallbackMap: An object where keys are the values of the radio buttons,
|
||||
* and values are dictionaries containing a 'callback' key and an optional 'element' key.
|
||||
* If provided, the element will be passed in as the second argument to the callback function.
|
||||
*
|
||||
* Usage Example:
|
||||
* Assuming you have radio buttons with values 'option1', 'option2', and 'option3',
|
||||
* and corresponding callback functions 'function1', 'function2', 'function3' that will
|
||||
* apply to elements 'element1', 'element2', 'element3' respectively.
|
||||
*
|
||||
* hookupCallbacksToRadioToggler('exampleRadioGroup', {
|
||||
* 'option1': {callback: function1, element: element1},
|
||||
* 'option2': {callback: function2, element: element2},
|
||||
* 'option3': {callback: function3} // No element provided
|
||||
* });
|
||||
*
|
||||
* Picking the 'option1' radio button will call function1('option1', element1).
|
||||
* Picking the 'option3' radio button will call function3('option3') without a second parameter.
|
||||
**/
|
||||
export function hookupCallbacksToRadioToggler(radioButtonName, valueToCallbackMap) {
|
||||
// Get the radio buttons
|
||||
let radioButtons = document.querySelectorAll(`input[name="${radioButtonName}"]`);
|
||||
|
||||
function handleRadioButtonChange() {
|
||||
// Find the checked radio button
|
||||
let radioButtonChecked = document.querySelector(`input[name="${radioButtonName}"]:checked`);
|
||||
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
||||
|
||||
// Execute the callback function for the selected value
|
||||
if (selectedValue && valueToCallbackMap[selectedValue]) {
|
||||
const entry = valueToCallbackMap[selectedValue];
|
||||
if ('element' in entry) {
|
||||
entry.callback(selectedValue, entry.element);
|
||||
} else {
|
||||
entry.callback(selectedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (radioButtons && radioButtons.length) {
|
||||
// Add event listener to each radio button
|
||||
radioButtons.forEach(function (radioButton) {
|
||||
radioButton.addEventListener('change', handleRadioButtonChange);
|
||||
});
|
||||
|
||||
// Initialize by checking the current state
|
||||
handleRadioButtonChange();
|
||||
}
|
||||
}
|
|
@ -99,9 +99,7 @@ body {
|
|||
}
|
||||
.section-outlined__search {
|
||||
flex-grow: 4;
|
||||
// Align right
|
||||
max-width: 383px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -190,6 +188,9 @@ abbr[title] {
|
|||
.visible-mobile-flex {
|
||||
display: none!important;
|
||||
}
|
||||
.text-right--tablet {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -286,3 +287,11 @@ Fit-content itself does not work.
|
|||
width: 3%;
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
|
||||
.width-40p {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.minh-143px {
|
||||
min-height: 143px;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,11 @@ th {
|
|||
border: none;
|
||||
}
|
||||
|
||||
td.padding-right-0,
|
||||
th.padding-right-0 {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
tr:first-child th:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
|
|
@ -89,52 +89,52 @@ urlpatterns = [
|
|||
name="members",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>",
|
||||
"member/<int:member_pk>",
|
||||
views.PortfolioMemberView.as_view(),
|
||||
name="member",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/delete",
|
||||
"member/<int:member_pk>/delete",
|
||||
views.PortfolioMemberDeleteView.as_view(),
|
||||
name="member-delete",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/permissions",
|
||||
"member/<int:member_pk>/permissions",
|
||||
views.PortfolioMemberEditView.as_view(),
|
||||
name="member-permissions",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/domains",
|
||||
"member/<int:member_pk>/domains",
|
||||
views.PortfolioMemberDomainsView.as_view(),
|
||||
name="member-domains",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/domains/edit",
|
||||
"member/<int:member_pk>/domains/edit",
|
||||
views.PortfolioMemberDomainsEditView.as_view(),
|
||||
name="member-domains-edit",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>",
|
||||
"invitedmember/<int:invitedmember_pk>",
|
||||
views.PortfolioInvitedMemberView.as_view(),
|
||||
name="invitedmember",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/delete",
|
||||
"invitedmember/<int:invitedmember_pk>/delete",
|
||||
views.PortfolioInvitedMemberDeleteView.as_view(),
|
||||
name="invitedmember-delete",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/permissions",
|
||||
"invitedmember/<int:invitedmember_pk>/permissions",
|
||||
views.PortfolioInvitedMemberEditView.as_view(),
|
||||
name="invitedmember-permissions",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/domains",
|
||||
"invitedmember/<int:invitedmember_pk>/domains",
|
||||
views.PortfolioInvitedMemberDomainsView.as_view(),
|
||||
name="invitedmember-domains",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/domains/edit",
|
||||
"invitedmember/<int:invitedmember_pk>/domains/edit",
|
||||
views.PortfolioInvitedMemberDomainsEditView.as_view(),
|
||||
name="invitedmember-domains-edit",
|
||||
),
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import logging
|
||||
import functools
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.decorators import method_decorator
|
||||
from registrar.models import Domain, DomainInformation, DomainInvitation, DomainRequest, UserDomainRole
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constants for clarity
|
||||
ALL = "all"
|
||||
IS_STAFF = "is_staff"
|
||||
IS_CISA_ANALYST = "is_cisa_analyst"
|
||||
IS_OMB_ANALYST = "is_omb_analyst"
|
||||
IS_FULL_ACCESS = "is_full_access"
|
||||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||
|
@ -98,24 +107,41 @@ def _user_has_permission(user, request, rules, **kwargs):
|
|||
if not user.is_authenticated or user.is_restricted():
|
||||
return False
|
||||
|
||||
portfolio = request.session.get("portfolio")
|
||||
# Define permission checks
|
||||
permission_checks = [
|
||||
(IS_STAFF, lambda: user.is_staff),
|
||||
(IS_DOMAIN_MANAGER, lambda: _is_domain_manager(user, **kwargs)),
|
||||
(IS_CISA_ANALYST, lambda: user.has_perm("registrar.analyst_access_permission")),
|
||||
(IS_OMB_ANALYST, lambda: user.groups.filter(name="omb_analysts_group").exists()),
|
||||
(IS_FULL_ACCESS, lambda: user.has_perm("registrar.full_access_permission")),
|
||||
(
|
||||
IS_DOMAIN_MANAGER,
|
||||
lambda: (not user.is_org_user(request) and _is_domain_manager(user, **kwargs))
|
||||
or (
|
||||
user.is_org_user(request)
|
||||
and _is_domain_manager(user, **kwargs)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk"))
|
||||
),
|
||||
),
|
||||
(IS_STAFF_MANAGING_DOMAIN, lambda: _is_staff_managing_domain(request, **kwargs)),
|
||||
(IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
|
||||
lambda: _has_portfolio_view_all_domains(request, kwargs.get("domain_pk")),
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_all_domains_portfolio_permission(portfolio)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_domains_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_any_domains_portfolio_permission(portfolio)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
|
||||
lambda: _is_domain_manager(user, **kwargs) and _is_portfolio_member(request),
|
||||
lambda: _is_domain_manager(user, **kwargs)
|
||||
and _is_portfolio_member(request)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
|
||||
|
@ -129,34 +155,55 @@ def _user_has_permission(user, request, rules, **kwargs):
|
|||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_any_requests_portfolio_permission(portfolio)
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_all_domain_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_view_all_domain_requests_portfolio_permission(portfolio)
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk")),
|
||||
lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk"))
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and (
|
||||
user.has_view_members_portfolio_permission(request.session.get("portfolio"))
|
||||
or user.has_edit_members_portfolio_permission(request.session.get("portfolio"))
|
||||
user.has_view_members_portfolio_permission(portfolio)
|
||||
or user.has_edit_members_portfolio_permission(portfolio)
|
||||
)
|
||||
and (
|
||||
# AND rather than OR because these functions return true if the PK is not found.
|
||||
# This adds support for if the view simply doesn't have said PK.
|
||||
_member_exists_under_portfolio(portfolio, kwargs.get("member_pk"))
|
||||
and _member_invitation_exists_under_portfolio(portfolio, kwargs.get("invitedmember_pk"))
|
||||
),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_edit_members_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_edit_members_portfolio_permission(portfolio)
|
||||
and (
|
||||
# AND rather than OR because these functions return true if the PK is not found.
|
||||
# This adds support for if the view simply doesn't have said PK.
|
||||
_member_exists_under_portfolio(portfolio, kwargs.get("member_pk"))
|
||||
and _member_invitation_exists_under_portfolio(portfolio, kwargs.get("invitedmember_pk"))
|
||||
),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_VIEW,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_members_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_view_members_portfolio_permission(portfolio)
|
||||
and (
|
||||
# AND rather than OR because these functions return true if the PK is not found.
|
||||
# This adds support for if the view simply doesn't have said PK.
|
||||
_member_exists_under_portfolio(portfolio, kwargs.get("member_pk"))
|
||||
and _member_invitation_exists_under_portfolio(portfolio, kwargs.get("invitedmember_pk"))
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -191,6 +238,70 @@ def _is_domain_manager(user, **kwargs):
|
|||
return False
|
||||
|
||||
|
||||
def _domain_exists_under_portfolio(portfolio, domain_pk):
|
||||
"""Checks to see if the given domain exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not domain_pk:
|
||||
logger.warning(
|
||||
"_domain_exists_under_portfolio => Could not find domain_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return Domain.objects.filter(domain_info__portfolio=portfolio, id=domain_pk).exists()
|
||||
|
||||
|
||||
def _domain_request_exists_under_portfolio(portfolio, domain_request_pk):
|
||||
"""Checks to see if the given domain request exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not domain_request_pk:
|
||||
logger.warning(
|
||||
"_domain_request_exists_under_portfolio => Could not find domain_request_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return DomainRequest.objects.filter(portfolio=portfolio, id=domain_request_pk).exists()
|
||||
|
||||
|
||||
def _member_exists_under_portfolio(portfolio, member_pk):
|
||||
"""Checks to see if the given UserPortfolioPermission exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not member_pk:
|
||||
logger.warning(
|
||||
"_member_exists_under_portfolio => Could not find member_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return UserPortfolioPermission.objects.filter(portfolio=portfolio, id=member_pk).exists()
|
||||
|
||||
|
||||
def _member_invitation_exists_under_portfolio(portfolio, invitedmember_pk):
|
||||
"""Checks to see if the given PortfolioInvitation exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not invitedmember_pk:
|
||||
logger.warning(
|
||||
"_member_invitation_exists_under_portfolio => Could not find invitedmember_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return PortfolioInvitation.objects.filter(portfolio=portfolio, id=invitedmember_pk).exists()
|
||||
|
||||
|
||||
def _is_domain_request_creator(user, domain_request_pk):
|
||||
"""Checks to see if the user is the creator of a domain request
|
||||
with domain_request_pk."""
|
||||
|
@ -286,15 +397,3 @@ def _is_staff_managing_domain(request, **kwargs):
|
|||
# the user is permissioned,
|
||||
# and it is in a valid status
|
||||
return True
|
||||
|
||||
|
||||
def _has_portfolio_view_all_domains(request, domain_pk):
|
||||
"""Returns whether the user in the request can access the domain
|
||||
via portfolio view all domains permission."""
|
||||
portfolio = request.session.get("portfolio")
|
||||
if request.user.has_view_all_domains_portfolio_permission(portfolio):
|
||||
if Domain.objects.filter(id=domain_pk).exists():
|
||||
domain = Domain.objects.get(id=domain_pk)
|
||||
if domain.domain_info.portfolio == portfolio:
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -319,31 +319,23 @@ class DomainRequestFixture:
|
|||
"""Creates DomainRequests given a list of users."""
|
||||
total_domain_requests_to_make = len(users) # 100000
|
||||
|
||||
# Check if the database is already populated with the desired
|
||||
# number of entries.
|
||||
# (Prevents re-adding more entries to an already populated database,
|
||||
# which happens when restarting Docker src)
|
||||
domain_requests_already_made = DomainRequest.objects.count()
|
||||
|
||||
domain_requests_to_create = []
|
||||
if domain_requests_already_made < total_domain_requests_to_make:
|
||||
for user in users:
|
||||
for request_data in cls.DOMAINREQUESTS:
|
||||
# Prepare DomainRequest objects
|
||||
try:
|
||||
domain_request = DomainRequest(
|
||||
creator=user,
|
||||
organization_name=request_data["organization_name"],
|
||||
)
|
||||
cls._set_non_foreign_key_fields(domain_request, request_data)
|
||||
cls._set_foreign_key_fields(domain_request, request_data, user)
|
||||
domain_requests_to_create.append(domain_request)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
num_additional_requests_to_make = (
|
||||
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create)
|
||||
)
|
||||
for user in users:
|
||||
for request_data in cls.DOMAINREQUESTS:
|
||||
# Prepare DomainRequest objects
|
||||
try:
|
||||
domain_request = DomainRequest(
|
||||
creator=user,
|
||||
organization_name=request_data["organization_name"],
|
||||
)
|
||||
cls._set_non_foreign_key_fields(domain_request, request_data)
|
||||
cls._set_foreign_key_fields(domain_request, request_data, user)
|
||||
domain_requests_to_create.append(domain_request)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
num_additional_requests_to_make = total_domain_requests_to_make - len(domain_requests_to_create)
|
||||
if num_additional_requests_to_make > 0:
|
||||
for _ in range(num_additional_requests_to_make):
|
||||
random_user = random.choice(users) # nosec
|
||||
|
|
|
@ -65,7 +65,12 @@ class DomainNameserverForm(forms.Form):
|
|||
|
||||
domain = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
server = forms.CharField(label="Name server", strip=True)
|
||||
server = forms.CharField(
|
||||
label="Name server",
|
||||
strip=True,
|
||||
required=True,
|
||||
error_messages={"required": "At least two name servers are required."},
|
||||
)
|
||||
|
||||
ip = forms.CharField(
|
||||
label="IP address (IPv4 or IPv6)",
|
||||
|
@ -76,13 +81,6 @@ class DomainNameserverForm(forms.Form):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super(DomainNameserverForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# add custom error messages
|
||||
self.fields["server"].error_messages.update(
|
||||
{
|
||||
"required": "At least two name servers are required.",
|
||||
}
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
# clean is called from clean_forms, which is called from is_valid
|
||||
# after clean_fields. it is used to determine form level errors.
|
||||
|
@ -183,43 +181,83 @@ class DomainSuborganizationForm(forms.ModelForm):
|
|||
|
||||
class BaseNameserverFormset(forms.BaseFormSet):
|
||||
def clean(self):
|
||||
"""
|
||||
Check for duplicate entries in the formset.
|
||||
"""
|
||||
"""Check for duplicate entries in the formset and ensure at least two valid nameservers."""
|
||||
error_message = "At least two name servers are required."
|
||||
|
||||
# Check if there are at least two valid servers
|
||||
valid_servers_count = sum(
|
||||
1 for form in self.forms if form.cleaned_data.get("server") and form.cleaned_data.get("server").strip()
|
||||
)
|
||||
if valid_servers_count >= 2:
|
||||
# If there are, remove the "At least two name servers are required" error from each form
|
||||
# This will allow for successful submissions when the first or second entries are blanked
|
||||
# but there are enough entries total
|
||||
for form in self.forms:
|
||||
if form.errors.get("server") == ["At least two name servers are required."]:
|
||||
form.errors.pop("server")
|
||||
valid_forms, invalid_forms, empty_forms = self._categorize_forms(error_message)
|
||||
self._enforce_minimum_nameservers(valid_forms, invalid_forms, empty_forms, error_message)
|
||||
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid on its own
|
||||
if any(self.errors): # Skip further validation if individual forms already have errors
|
||||
return
|
||||
|
||||
data = []
|
||||
self._check_for_duplicates()
|
||||
|
||||
def _categorize_forms(self, error_message):
|
||||
"""Sort forms into valid, invalid or empty based on the 'server' field."""
|
||||
valid_forms = []
|
||||
invalid_forms = []
|
||||
empty_forms = []
|
||||
|
||||
for form in self.forms:
|
||||
if not self._is_server_validation_needed(form, error_message):
|
||||
invalid_forms.append(form)
|
||||
continue
|
||||
server = form.cleaned_data.get("server", "").strip()
|
||||
if server:
|
||||
valid_forms.append(form)
|
||||
else:
|
||||
empty_forms.append(form)
|
||||
|
||||
return valid_forms, invalid_forms, empty_forms
|
||||
|
||||
def _is_server_validation_needed(self, form, error_message):
|
||||
"""Determine if server validation should be performed on a given form."""
|
||||
return form.is_valid() or list(form.errors.get("server", [])) == [error_message]
|
||||
|
||||
def _enforce_minimum_nameservers(self, valid_forms, invalid_forms, empty_forms, error_message):
|
||||
"""Ensure at least two nameservers are provided, adjusting error messages as needed."""
|
||||
if len(valid_forms) + len(invalid_forms) < 2:
|
||||
self._add_required_error(empty_forms, error_message)
|
||||
else:
|
||||
self._remove_required_error_from_forms(error_message)
|
||||
|
||||
def _add_required_error(self, empty_forms, error_message):
|
||||
"""Add 'At least two name servers' error to one form and remove duplicates."""
|
||||
error_added = False
|
||||
|
||||
for form in empty_forms:
|
||||
if list(form.errors.get("server", [])) == [error_message]:
|
||||
form.errors.pop("server")
|
||||
|
||||
if not error_added:
|
||||
form.add_error("server", error_message)
|
||||
error_added = True
|
||||
|
||||
def _remove_required_error_from_forms(self, error_message):
|
||||
"""Remove the 'At least two name servers' error from all forms if sufficient nameservers exist."""
|
||||
for form in self.forms:
|
||||
if form.errors.get("server") == [error_message]:
|
||||
form.errors.pop("server")
|
||||
|
||||
def _check_for_duplicates(self):
|
||||
"""Ensure no duplicate nameservers exist within the formset."""
|
||||
seen_servers = set()
|
||||
duplicates = []
|
||||
|
||||
for index, form in enumerate(self.forms):
|
||||
if form.cleaned_data:
|
||||
value = form.cleaned_data["server"]
|
||||
# We need to make sure not to trigger the duplicate error in case the first and second nameservers
|
||||
# are empty. If there are enough records in the formset, that error is an unecessary blocker.
|
||||
# If there aren't, the required error will block the submit.
|
||||
if value in data and not (form.cleaned_data.get("server", "").strip() == "" and index == 1):
|
||||
form.add_error(
|
||||
"server",
|
||||
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),
|
||||
)
|
||||
duplicates.append(value)
|
||||
else:
|
||||
data.append(value)
|
||||
for form in self.forms:
|
||||
if not form.cleaned_data:
|
||||
continue
|
||||
|
||||
server = form.cleaned_data["server"].strip()
|
||||
|
||||
if server and server in seen_servers:
|
||||
form.add_error(
|
||||
"server",
|
||||
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=server),
|
||||
)
|
||||
duplicates.append(server)
|
||||
else:
|
||||
seen_servers.add(server)
|
||||
|
||||
|
||||
NameserverFormset = formset_factory(
|
||||
|
|
|
@ -86,7 +86,6 @@ class RequestingEntityForm(RegistrarForm):
|
|||
return {}
|
||||
# get the domain request as a dict, per usual method
|
||||
domain_request_dict = {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||
|
||||
# set sub_organization to 'other' if is_requesting_new_suborganization is True
|
||||
if isinstance(obj, DomainRequest) and obj.is_requesting_new_suborganization():
|
||||
domain_request_dict["sub_organization"] = "other"
|
||||
|
@ -348,7 +347,7 @@ class OrganizationContactForm(RegistrarForm):
|
|||
error_messages={
|
||||
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||
},
|
||||
widget=ComboboxWidget,
|
||||
widget=ComboboxWidget(attrs={"required": True}),
|
||||
)
|
||||
zipcode = forms.CharField(
|
||||
label="Zip code",
|
||||
|
@ -608,7 +607,10 @@ class DotGovDomainForm(RegistrarForm):
|
|||
)
|
||||
|
||||
|
||||
class PurposeForm(RegistrarForm):
|
||||
class PurposeDetailsForm(BaseDeletableRegistrarForm):
|
||||
|
||||
field_name = "purpose"
|
||||
|
||||
purpose = forms.CharField(
|
||||
label="Purpose",
|
||||
widget=forms.Textarea(
|
||||
|
|
123
src/registrar/forms/feb.py
Normal file
123
src/registrar/forms/feb.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
from django import forms
|
||||
from django.core.validators import MaxLengthValidator
|
||||
from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm
|
||||
|
||||
|
||||
class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm):
|
||||
"""
|
||||
Form for verifying if the domain request meets the Federal Executive Branch domain naming requirements.
|
||||
If the "no" option is selected, details must be provided via the separate details form.
|
||||
"""
|
||||
|
||||
field_name = "feb_naming_requirements"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||
"""
|
||||
return self.domain_request.feb_naming_requirements
|
||||
|
||||
|
||||
class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
|
||||
# Text area for additional details; rendered conditionally when "no" is selected.
|
||||
feb_naming_requirements_details = forms.CharField(
|
||||
widget=forms.Textarea(attrs={"maxlength": "2000"}),
|
||||
max_length=2000,
|
||||
required=True,
|
||||
error_messages={"required": ("This field is required.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
label="",
|
||||
help_text="Maximum 2000 characters allowed.",
|
||||
)
|
||||
|
||||
|
||||
class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
|
||||
|
||||
field_name = "feb_purpose_choice"
|
||||
|
||||
form_choices = (
|
||||
("new", "Used for a new website"),
|
||||
("redirect", "Used as a redirect for an existing website"),
|
||||
("other", "Not for a website"),
|
||||
)
|
||||
|
||||
feb_purpose_choice = forms.ChoiceField(
|
||||
required=True,
|
||||
choices=form_choices,
|
||||
widget=forms.RadioSelect,
|
||||
error_messages={
|
||||
"required": "This question is required.",
|
||||
},
|
||||
label="Select one",
|
||||
)
|
||||
|
||||
|
||||
class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
|
||||
"""
|
||||
Form for determining whether the domain request comes with a target timeframe for launch.
|
||||
If the "no" option is selected, details must be provided via the separate details form.
|
||||
"""
|
||||
|
||||
field_name = "has_timeframe"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||
"""
|
||||
return self.domain_request.has_timeframe
|
||||
|
||||
|
||||
class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
|
||||
time_frame_details = forms.CharField(
|
||||
label="time_frame_details",
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"aria-label": "Provide details on your target timeframe. \
|
||||
Is there a special significance to this date (legal requirement, announcement, event, etc)?"
|
||||
}
|
||||
),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": "Provide details on your target timeframe."},
|
||||
)
|
||||
|
||||
|
||||
class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
|
||||
"""
|
||||
Form for determining whether the domain request is part of an interagency initative.
|
||||
If the "no" option is selected, details must be provided via the separate details form.
|
||||
"""
|
||||
|
||||
field_name = "is_interagency_initiative"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||
"""
|
||||
return self.domain_request.is_interagency_initiative
|
||||
|
||||
|
||||
class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
|
||||
interagency_initiative_details = forms.CharField(
|
||||
label="interagency_initiative_details",
|
||||
widget=forms.Textarea(attrs={"aria-label": "Name the agencies that will be involved in this initiative."}),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": "Name the agencies that will be involved in this initiative."},
|
||||
)
|
|
@ -22,6 +22,7 @@ from registrar.models.utility.portfolio_helper import (
|
|||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
get_portfolio_invitation_associations,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -445,3 +446,35 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm):
|
|||
class Meta:
|
||||
model = PortfolioInvitation
|
||||
fields = ["portfolio", "email", "roles", "additional_permissions"]
|
||||
|
||||
def _post_clean(self):
|
||||
"""
|
||||
Override _post_clean to customize model validation errors.
|
||||
This runs after form clean is complete, but before the errors are displayed.
|
||||
"""
|
||||
try:
|
||||
super()._post_clean()
|
||||
self.instance.clean()
|
||||
except forms.ValidationError as e:
|
||||
override_error = False
|
||||
if hasattr(e, "code"):
|
||||
field = "email" if "email" in self.fields else None
|
||||
if e.code == "has_existing_permissions":
|
||||
existing_permissions, existing_invitations = get_portfolio_invitation_associations(self.instance)
|
||||
|
||||
same_portfolio_for_permissions = existing_permissions.exclude(portfolio=self.instance.portfolio)
|
||||
same_portfolio_for_invitations = existing_invitations.exclude(portfolio=self.instance.portfolio)
|
||||
if same_portfolio_for_permissions.exists() or same_portfolio_for_invitations.exists():
|
||||
self.add_error(
|
||||
field, f"{self.instance.email} is already a member of another .gov organization."
|
||||
)
|
||||
override_error = True
|
||||
elif e.code == "has_existing_invitations":
|
||||
self.add_error(
|
||||
field, f"{self.instance.email} has already been invited to another .gov organization."
|
||||
)
|
||||
override_error = True
|
||||
|
||||
# Errors denoted as "__all__" are special error types reserved for the model level clean function
|
||||
if override_error and "__all__" in self._errors:
|
||||
del self._errors["__all__"]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
""" "
|
||||
"""
|
||||
Converts all ready and DNS needed domains with a non-default public contact
|
||||
to disclose their public contact. Created for Issue#1535 to resolve
|
||||
disclose issue of domains with missing security emails.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
""" "
|
||||
"""
|
||||
Data migration: Renaming deprecated Federal Agencies to
|
||||
their new updated names ie (U.S. Peace Corps to Peace Corps)
|
||||
within Domain Information and Domain Requests
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
# Generated by Django 4.2.17 on 2025-02-28 17:11
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0140_alter_portfolioinvitation_additional_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="portfolioinvitation",
|
||||
name="additional_permissions",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[
|
||||
("view_all_domains", "Viewer"),
|
||||
("view_managed_domains", "Viewer, limited (domains they manage)"),
|
||||
("view_members", "Viewer"),
|
||||
("edit_members", "Manager"),
|
||||
("view_all_requests", "Viewer"),
|
||||
("edit_requests", "Creator"),
|
||||
("view_portfolio", "Viewer"),
|
||||
("edit_portfolio", "Manager"),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more additional permissions.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolioinvitation",
|
||||
name="roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[("organization_admin", "Admin"), ("organization_member", "Basic")], max_length=50
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more roles.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userportfoliopermission",
|
||||
name="additional_permissions",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[
|
||||
("view_all_domains", "Viewer"),
|
||||
("view_managed_domains", "Viewer, limited (domains they manage)"),
|
||||
("view_members", "Viewer"),
|
||||
("edit_members", "Manager"),
|
||||
("view_all_requests", "Viewer"),
|
||||
("edit_requests", "Creator"),
|
||||
("view_portfolio", "Viewer"),
|
||||
("edit_portfolio", "Manager"),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more additional permissions.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userportfoliopermission",
|
||||
name="roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[("organization_admin", "Admin"), ("organization_member", "Basic")], max_length=50
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more roles.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,50 @@
|
|||
# Generated by Django 4.2.17 on 2025-03-10 19:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0141_alter_portfolioinvitation_additional_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="feb_naming_requirements",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="feb_naming_requirements_details",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="feb_purpose_choice",
|
||||
field=models.CharField(
|
||||
blank=True, choices=[("website", "Website"), ("redirect", "Redirect"), ("other", "Other")], null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="has_timeframe",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="interagency_initiative_details",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="is_interagency_initiative",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="time_frame_details",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
38
src/registrar/migrations/0143_create_groups_v18.py
Normal file
38
src/registrar/migrations/0143_create_groups_v18.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0079 (which populates federal agencies)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_omb_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0142_domainrequest_feb_naming_requirements_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -245,6 +245,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
is called in the validate function on the request/domain page
|
||||
|
||||
throws- RegistryError or InvalidDomainError"""
|
||||
|
||||
if not cls.string_could_be_domain(domain):
|
||||
logger.warning("Not a valid domain: %s" % str(domain))
|
||||
# throw invalid domain error so that it can be caught in
|
||||
|
@ -879,6 +880,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
which inturn call this function)
|
||||
Will throw error if contact type is not the same as expectType
|
||||
Raises ValueError if expected type doesn't match the contact type"""
|
||||
|
||||
if expectedType != contact.contact_type:
|
||||
raise ValueError("Cannot set a contact with a different contact type, expected type was %s" % expectedType)
|
||||
|
||||
|
@ -891,7 +893,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
duplicate_contacts = PublicContact.objects.exclude(registry_id=contact.registry_id).filter(
|
||||
domain=self, contact_type=contact.contact_type
|
||||
)
|
||||
|
||||
# if no record exists with this contact type
|
||||
# make contact in registry, duplicate and errors handled there
|
||||
errorCode = self._make_contact_in_registry(contact)
|
||||
|
@ -970,6 +971,24 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
logger.info("making technical contact")
|
||||
self._set_singleton_contact(contact, expectedType=contact.ContactTypeChoices.TECHNICAL)
|
||||
|
||||
def print_contact_info_epp(self, contact: PublicContact):
|
||||
"""Prints registry data for this PublicContact for easier debugging"""
|
||||
results = self._request_contact_info(contact, get_result_as_dict=True)
|
||||
logger.info("---------------------")
|
||||
logger.info(f"EPP info for {contact.contact_type}:")
|
||||
logger.info("---------------------")
|
||||
for key, value in results.items():
|
||||
logger.info(f"{key}: {value}")
|
||||
|
||||
def print_all_domain_contact_info_epp(self):
|
||||
"""Prints registry data for this domains security, registrant, technical, and administrative contacts."""
|
||||
logger.info(f"Contact info for {self}:")
|
||||
logger.info("=====================")
|
||||
contacts = [self.security_contact, self.registrant_contact, self.technical_contact, self.administrative_contact]
|
||||
for contact in contacts:
|
||||
if contact:
|
||||
self.print_contact_info_epp(contact)
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Currently just returns if the state is created,
|
||||
because then it should be live, theoretically.
|
||||
|
@ -1350,10 +1369,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
)
|
||||
return street_dict
|
||||
|
||||
def _request_contact_info(self, contact: PublicContact):
|
||||
def _request_contact_info(self, contact: PublicContact, get_result_as_dict=False):
|
||||
"""Grabs the resultant contact information in epp for this public contact
|
||||
by using the InfoContact command.
|
||||
Returns a commands.InfoContactResultData object, or a dict if get_result_as_dict is True."""
|
||||
try:
|
||||
req = commands.InfoContact(id=contact.registry_id)
|
||||
return registry.send(req, cleaned=True).res_data[0]
|
||||
result = registry.send(req, cleaned=True).res_data[0]
|
||||
return result if not get_result_as_dict else vars(result)
|
||||
except RegistryError as error:
|
||||
logger.error(
|
||||
"Registry threw error for contact id %s contact type is %s, error code is\n %s full error is %s", # noqa
|
||||
|
@ -1673,22 +1696,26 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
return help_text
|
||||
|
||||
def _disclose_fields(self, contact: PublicContact):
|
||||
"""creates a disclose object that can be added to a contact Create using
|
||||
"""creates a disclose object that can be added to a contact Create or Update using
|
||||
.disclose= <this function> on the command before sending.
|
||||
if item is security email then make sure email is visible"""
|
||||
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
|
||||
# You can find each enum here:
|
||||
# https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32
|
||||
DF = epp.DiscloseField
|
||||
fields = {DF.EMAIL}
|
||||
all_disclose_fields = {field for field in DF}
|
||||
disclose_args = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}}
|
||||
|
||||
hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
|
||||
disclose = is_security and contact.email not in hidden_security_emails
|
||||
# Delete after testing on other devices
|
||||
logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose)
|
||||
# Will only disclose DF.EMAIL if its not the default
|
||||
return epp.Disclose(
|
||||
flag=disclose,
|
||||
fields=fields,
|
||||
)
|
||||
fields_to_remove = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT}
|
||||
if contact.contact_type == contact.ContactTypeChoices.SECURITY:
|
||||
if contact.email not in DefaultEmail.get_all_emails():
|
||||
fields_to_remove.add(DF.EMAIL)
|
||||
elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE:
|
||||
fields_to_remove.update({DF.EMAIL, DF.VOICE, DF.ADDR})
|
||||
|
||||
disclose_args["fields"].difference_update(fields_to_remove) # type: ignore
|
||||
|
||||
logger.debug("Updated domain contact %s to disclose: %s", contact.email, disclose_args.get("flag"))
|
||||
return epp.Disclose(**disclose_args) # type: ignore
|
||||
|
||||
def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore
|
||||
return epp.PostalInfo( # type: ignore
|
||||
|
|
|
@ -449,7 +449,9 @@ class DomainInformation(TimeStampedModel):
|
|||
def converted_federal_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type
|
||||
return self.federal_type
|
||||
elif self.federal_agency:
|
||||
return self.federal_agency.federal_type
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_senior_official(self):
|
||||
|
|
|
@ -54,6 +54,11 @@ class DomainRequest(TimeStampedModel):
|
|||
"""Returns the associated label for a given status name"""
|
||||
return cls(status_name).label if status_name else None
|
||||
|
||||
class FEBPurposeChoices(models.TextChoices):
|
||||
WEBSITE = "website"
|
||||
REDIRECT = "redirect"
|
||||
OTHER = "other"
|
||||
|
||||
class StateTerritoryChoices(models.TextChoices):
|
||||
ALABAMA = "AL", "Alabama (AL)"
|
||||
ALASKA = "AK", "Alaska (AK)"
|
||||
|
@ -501,6 +506,51 @@ class DomainRequest(TimeStampedModel):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
# Fields specific to Federal Executive Branch agencies, used by OMB for reviewing requests
|
||||
feb_naming_requirements = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
feb_naming_requirements_details = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
feb_purpose_choice = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
choices=FEBPurposeChoices.choices,
|
||||
)
|
||||
|
||||
# This field is alternately used for generic domain purpose explanations
|
||||
# and for explanations of the specific purpose chosen with feb_purpose_choice
|
||||
# by a Federal Executive Branch agency.
|
||||
purpose = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
has_timeframe = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
time_frame_details = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
is_interagency_initiative = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
interagency_initiative_details = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
alternative_domains = models.ManyToManyField(
|
||||
"registrar.Website",
|
||||
blank=True,
|
||||
|
@ -508,11 +558,6 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Other domain names the creator provided for consideration",
|
||||
)
|
||||
|
||||
purpose = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
other_contacts = models.ManyToManyField(
|
||||
"registrar.Contact",
|
||||
blank=True,
|
||||
|
@ -1389,6 +1434,12 @@ class DomainRequest(TimeStampedModel):
|
|||
has_details = False
|
||||
return has_details
|
||||
|
||||
def is_feb(self) -> bool:
|
||||
"""Is this domain request for a Federal Executive Branch agency?"""
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type == BranchChoices.EXECUTIVE
|
||||
return False
|
||||
|
||||
def is_federal(self) -> Union[bool, None]:
|
||||
"""Is this domain request for a federal agency?
|
||||
|
||||
|
@ -1454,7 +1505,9 @@ class DomainRequest(TimeStampedModel):
|
|||
def converted_federal_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type
|
||||
return self.federal_type
|
||||
elif self.federal_agency:
|
||||
return self.federal_agency.federal_type
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_address_line1(self):
|
||||
|
|
|
@ -15,6 +15,7 @@ from .utility.portfolio_helper import (
|
|||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
get_readable_roles,
|
||||
get_role_display,
|
||||
validate_portfolio_invitation,
|
||||
) # type: ignore
|
||||
|
@ -78,6 +79,10 @@ class PortfolioInvitation(TimeStampedModel):
|
|||
def __str__(self):
|
||||
return f"Invitation for {self.email} on {self.portfolio} is {self.status}"
|
||||
|
||||
def get_readable_roles(self):
|
||||
"""Returns a readable list of self.roles"""
|
||||
return get_readable_roles(self.roles)
|
||||
|
||||
def get_managed_domains_count(self):
|
||||
"""Return the count of domain invitations managed by the invited user for this portfolio."""
|
||||
# Filter the UserDomainRole model to get domains where the user has a manager role
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from random import choices
|
||||
from string import ascii_uppercase, ascii_lowercase, digits
|
||||
|
@ -9,6 +10,9 @@ from registrar.utility.enums import DefaultEmail
|
|||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_id():
|
||||
"""Generate a 16 character registry ID with a low probability of collision."""
|
||||
day = datetime.today().strftime("%A")[:2]
|
||||
|
@ -92,15 +96,14 @@ class PublicContact(TimeStampedModel):
|
|||
return cls(
|
||||
contact_type=PublicContact.ContactTypeChoices.REGISTRANT,
|
||||
registry_id=get_id(),
|
||||
name="CSD/CB – Attn: Cameron Dixon",
|
||||
name="CSD/CB – Attn: .gov TLD",
|
||||
org="Cybersecurity and Infrastructure Security Agency",
|
||||
street1="CISA – NGR STOP 0645",
|
||||
street2="1110 N. Glebe Rd.",
|
||||
street1="1110 N. Glebe Rd",
|
||||
city="Arlington",
|
||||
sp="VA",
|
||||
pc="20598-0645",
|
||||
pc="22201",
|
||||
cc="US",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
@ -110,14 +113,14 @@ class PublicContact(TimeStampedModel):
|
|||
return cls(
|
||||
contact_type=PublicContact.ContactTypeChoices.ADMINISTRATIVE,
|
||||
registry_id=get_id(),
|
||||
name="Program Manager",
|
||||
name="CSD/CB – Attn: .gov TLD",
|
||||
org="Cybersecurity and Infrastructure Security Agency",
|
||||
street1="4200 Wilson Blvd.",
|
||||
street1="1110 N. Glebe Rd",
|
||||
city="Arlington",
|
||||
sp="VA",
|
||||
pc="22201",
|
||||
cc="US",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
@ -127,14 +130,14 @@ class PublicContact(TimeStampedModel):
|
|||
return cls(
|
||||
contact_type=PublicContact.ContactTypeChoices.TECHNICAL,
|
||||
registry_id=get_id(),
|
||||
name="Registry Customer Service",
|
||||
name="CSD/CB – Attn: .gov TLD",
|
||||
org="Cybersecurity and Infrastructure Security Agency",
|
||||
street1="4200 Wilson Blvd.",
|
||||
street1="1110 N. Glebe Rd",
|
||||
city="Arlington",
|
||||
sp="VA",
|
||||
pc="22201",
|
||||
cc="US",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
@ -144,14 +147,14 @@ class PublicContact(TimeStampedModel):
|
|||
return cls(
|
||||
contact_type=PublicContact.ContactTypeChoices.SECURITY,
|
||||
registry_id=get_id(),
|
||||
name="Registry Customer Service",
|
||||
name="CSD/CB – Attn: .gov TLD",
|
||||
org="Cybersecurity and Infrastructure Security Agency",
|
||||
street1="4200 Wilson Blvd.",
|
||||
street1="1110 N. Glebe Rd",
|
||||
city="Arlington",
|
||||
sp="VA",
|
||||
pc="22201",
|
||||
cc="US",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
|
|
@ -141,6 +141,99 @@ class UserGroup(Group):
|
|||
except Exception as e:
|
||||
logger.error(f"Error creating analyst permissions group: {e}")
|
||||
|
||||
def create_omb_analyst_group(apps, schema_editor):
|
||||
"""This method gets run from a data migration."""
|
||||
|
||||
# Hard to pass self to these methods as the calls from migrations
|
||||
# are only expecting apps and schema_editor, so we'll just define
|
||||
# apps, schema_editor in the local scope instead
|
||||
OMB_ANALYST_GROUP_PERMISSIONS = [
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domainrequest",
|
||||
"permissions": ["change_domainrequest"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domain",
|
||||
"permissions": ["view_domain"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domaininvitation",
|
||||
"permissions": ["view_domaininvitation"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "federalagency",
|
||||
"permissions": ["view_federalagency"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "portfolio",
|
||||
"permissions": ["view_portfolio"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "suborganization",
|
||||
"permissions": ["view_suborganization"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "seniorofficial",
|
||||
"permissions": ["view_seniorofficial"],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
# of the 'atomic' block.
|
||||
# From django docs:
|
||||
# https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations
|
||||
# We can’t import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
UserGroup = apps.get_model("registrar", "UserGroup")
|
||||
|
||||
logger.info("Going to create the OMB Analyst Group")
|
||||
try:
|
||||
omb_analysts_group, _ = UserGroup.objects.get_or_create(
|
||||
name="omb_analysts_group",
|
||||
)
|
||||
|
||||
omb_analysts_group.permissions.clear()
|
||||
|
||||
for permission in OMB_ANALYST_GROUP_PERMISSIONS:
|
||||
app_label = permission["app_label"]
|
||||
model_name = permission["model"]
|
||||
permissions = permission["permissions"]
|
||||
|
||||
# Retrieve the content type for the app and model
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model_name)
|
||||
|
||||
# Retrieve the permissions based on their codenames
|
||||
permissions = Permission.objects.filter(content_type=content_type, codename__in=permissions)
|
||||
|
||||
# Assign the permissions to the group
|
||||
omb_analysts_group.permissions.add(*permissions)
|
||||
|
||||
# Convert the permissions QuerySet to a list of codenames
|
||||
permission_list = list(permissions.values_list("codename", flat=True))
|
||||
|
||||
logger.debug(
|
||||
app_label
|
||||
+ " | "
|
||||
+ model_name
|
||||
+ " | "
|
||||
+ ", ".join(permission_list)
|
||||
+ " added to group "
|
||||
+ omb_analysts_group.name
|
||||
)
|
||||
|
||||
logger.debug("OMB Analyst permissions added to group " + omb_analysts_group.name)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating analyst permissions group: {e}")
|
||||
|
||||
def create_full_access_group(apps, schema_editor):
|
||||
"""This method gets run from a data migration."""
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ from registrar.models.utility.portfolio_helper import (
|
|||
get_domains_description_display,
|
||||
get_members_display,
|
||||
get_members_description_display,
|
||||
get_readable_roles,
|
||||
get_role_display,
|
||||
validate_user_portfolio_permission,
|
||||
)
|
||||
|
@ -94,12 +95,7 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
|
||||
def get_readable_roles(self):
|
||||
"""Returns a readable list of self.roles"""
|
||||
readable_roles = []
|
||||
if self.roles:
|
||||
readable_roles = sorted(
|
||||
[UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in self.roles]
|
||||
)
|
||||
return readable_roles
|
||||
return get_readable_roles(self.roles)
|
||||
|
||||
def get_managed_domains_count(self):
|
||||
"""Return the count of domains managed by the user for this portfolio."""
|
||||
|
@ -275,7 +271,12 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
def clean(self):
|
||||
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
||||
super().clean()
|
||||
validate_user_portfolio_permission(self)
|
||||
# Ensure user exists before running further validation
|
||||
# In django admin, this clean method is called before form validation checks
|
||||
# for required fields. Since validation below requires user, skip if user does
|
||||
# not exist
|
||||
if self.user_id:
|
||||
validate_user_portfolio_permission(self)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
|||
"""
|
||||
|
||||
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
||||
ORGANIZATION_MEMBER = "organization_member", "Member"
|
||||
ORGANIZATION_MEMBER = "organization_member", "Basic"
|
||||
|
||||
@classmethod
|
||||
def get_user_portfolio_role_label(cls, user_portfolio_role):
|
||||
|
@ -30,17 +30,17 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
|||
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||
""" """
|
||||
|
||||
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
|
||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
|
||||
VIEW_ALL_DOMAINS = "view_all_domains", "Viewer"
|
||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "Viewer, limited (domains they manage)"
|
||||
|
||||
VIEW_MEMBERS = "view_members", "View members"
|
||||
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
||||
VIEW_MEMBERS = "view_members", "Viewer"
|
||||
EDIT_MEMBERS = "edit_members", "Manager"
|
||||
|
||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||
VIEW_ALL_REQUESTS = "view_all_requests", "Viewer"
|
||||
EDIT_REQUESTS = "edit_requests", "Creator"
|
||||
|
||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
|
||||
VIEW_PORTFOLIO = "view_portfolio", "Viewer"
|
||||
EDIT_PORTFOLIO = "edit_portfolio", "Manager"
|
||||
|
||||
@classmethod
|
||||
def get_user_portfolio_permission_label(cls, user_portfolio_permission):
|
||||
|
@ -79,6 +79,13 @@ class MemberPermissionDisplay(StrEnum):
|
|||
NONE = "None"
|
||||
|
||||
|
||||
def get_readable_roles(roles):
|
||||
readable_roles = []
|
||||
if roles:
|
||||
readable_roles = sorted([UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in roles])
|
||||
return readable_roles
|
||||
|
||||
|
||||
def get_role_display(roles):
|
||||
"""
|
||||
Returns a user-friendly display name for a given list of user roles.
|
||||
|
@ -250,9 +257,6 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
Raises:
|
||||
ValidationError: If any of the validation rules are violated.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
|
||||
has_portfolio = bool(user_portfolio_permission.portfolio_id)
|
||||
portfolio_permissions = set(user_portfolio_permission._get_portfolio_permissions())
|
||||
|
||||
|
@ -279,26 +283,50 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
|
||||
# == Validate the multiple_porfolios flag. == #
|
||||
if not flag_is_active_for_user(user_portfolio_permission.user, "multiple_portfolios"):
|
||||
existing_permissions = UserPortfolioPermission.objects.exclude(id=user_portfolio_permission.id).filter(
|
||||
user=user_portfolio_permission.user
|
||||
existing_permissions, existing_invitations = get_user_portfolio_permission_associations(
|
||||
user_portfolio_permission
|
||||
)
|
||||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_permissions",
|
||||
)
|
||||
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email=user_portfolio_permission.user.email).exclude(
|
||||
Q(portfolio=user_portfolio_permission.portfolio)
|
||||
| Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
if existing_invitations.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio invitation. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_invitations",
|
||||
)
|
||||
|
||||
|
||||
def get_user_portfolio_permission_associations(user_portfolio_permission):
|
||||
"""
|
||||
Retrieves the associations for a user portfolio invitation.
|
||||
|
||||
Returns:
|
||||
A tuple:
|
||||
(existing_permissions, existing_invitations)
|
||||
where:
|
||||
- existing_permissions: UserPortfolioPermission objects excluding the current permission.
|
||||
- existing_invitations: PortfolioInvitation objects for the user email excluding
|
||||
the current invitation and those with status RETRIEVED.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
existing_permissions = UserPortfolioPermission.objects.exclude(id=user_portfolio_permission.id).filter(
|
||||
user=user_portfolio_permission.user
|
||||
)
|
||||
existing_invitations = PortfolioInvitation.objects.filter(
|
||||
email__iexact=user_portfolio_permission.user.email
|
||||
).exclude(
|
||||
Q(portfolio=user_portfolio_permission.portfolio)
|
||||
| Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
return (existing_permissions, existing_invitations)
|
||||
|
||||
|
||||
def validate_portfolio_invitation(portfolio_invitation):
|
||||
"""
|
||||
Validates a PortfolioInvitation instance. Located in portfolio_helper to avoid circular imports
|
||||
|
@ -315,7 +343,6 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
Raises:
|
||||
ValidationError: If any of the validation rules are violated.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -342,29 +369,48 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
)
|
||||
|
||||
# == Validate the multiple_porfolios flag. == #
|
||||
user = User.objects.filter(email=portfolio_invitation.email).first()
|
||||
user = User.objects.filter(email__iexact=portfolio_invitation.email).first()
|
||||
|
||||
# If user returns None, then we check for global assignment of multiple_portfolios.
|
||||
# Otherwise we just check on the user.
|
||||
if not flag_is_active_for_user(user, "multiple_portfolios"):
|
||||
existing_permissions = UserPortfolioPermission.objects.filter(user=user)
|
||||
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email=portfolio_invitation.email).exclude(
|
||||
Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
|
||||
existing_permissions, existing_invitations = get_portfolio_invitation_associations(portfolio_invitation)
|
||||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_permissions",
|
||||
)
|
||||
|
||||
if existing_invitations.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio invitation. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_invitations",
|
||||
)
|
||||
|
||||
|
||||
def get_portfolio_invitation_associations(portfolio_invitation):
|
||||
"""
|
||||
Retrieves the associations for a portfolio invitation.
|
||||
|
||||
Returns:
|
||||
A tuple:
|
||||
(existing_permissions, existing_invitations)
|
||||
where:
|
||||
- existing_permissions: UserPortfolioPermission objects matching the email.
|
||||
- existing_invitations: PortfolioInvitation objects for the email excluding
|
||||
the current invitation and those with status RETRIEVED.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
existing_permissions = UserPortfolioPermission.objects.filter(user__email__iexact=portfolio_invitation.email)
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email__iexact=portfolio_invitation.email).exclude(
|
||||
Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
return (existing_permissions, existing_invitations)
|
||||
|
||||
|
||||
def cleanup_after_portfolio_member_deletion(portfolio, email, user=None):
|
||||
"""
|
||||
Cleans up after removing a portfolio member or a portfolio invitation.
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if perms.registrar.analyst_access_permission or perms.full_access_permission %}
|
||||
<div class="module">
|
||||
<table class="width-full">
|
||||
<caption class="text-bold">Analytics</caption>
|
||||
|
@ -78,6 +79,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
{% include "django/admin/includes/descriptions/verified_by_staff_description.html" %}
|
||||
{% elif opts.model_name == 'website' %}
|
||||
{% include "django/admin/includes/descriptions/website_description.html" %}
|
||||
{% elif opts.model_name == 'userportfoliopermission' %}
|
||||
{% include "django/admin/includes/descriptions/user_portfolio_permission_description.html" %}
|
||||
{% elif opts.model_name == 'portfolioinvitation' %}
|
||||
{% include "django/admin/includes/descriptions/portfolio_invitation_description.html" %}
|
||||
{% elif opts.model_name == 'allowedemail' %}
|
||||
|
|
|
@ -11,13 +11,15 @@
|
|||
{% block field_sets %}
|
||||
<div class="display-flex flex-row flex-justify submit-row">
|
||||
<div class="flex-align-self-start button-list-mobile">
|
||||
{% if not adminform.form.is_omb_analyst %}
|
||||
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
||||
{# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
|
||||
<span class="mini-spacer"></span>
|
||||
<input type="submit" value="Get registry status" name="_get_status">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="desktop:flex-align-self-end">
|
||||
{% if original.state != original.State.DELETED %}
|
||||
{% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
|
||||
<a class="text-middle" href="#toggle-extend-expiration-alert" aria-controls="toggle-extend-expiration-alert" data-open-modal>
|
||||
Extend expiration date
|
||||
</a>
|
||||
|
@ -31,9 +33,11 @@
|
|||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||
{% endif %}
|
||||
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
||||
{% if not adminform.form.is_omb_analyst %}
|
||||
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||
{% endif %}
|
||||
{% if original.state != original.State.DELETED %}
|
||||
{% endif %}
|
||||
{% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
|
||||
<a class="text-middle" href="#toggle-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal>
|
||||
Remove from registry
|
||||
</a>
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you add someone to a domain here, it will trigger emails to the invitee and all managers of the domain when you click "save." If you don't want to trigger those emails, use the <a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User domain roles permissions table</a> instead.
|
||||
If you invite someone to a domain here, it will trigger email notifications. If you don't want to trigger emails, use the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">
|
||||
User Domain Roles
|
||||
</a>
|
||||
table instead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the domain invitation here, it won't trigger any emails. It also won't remove
|
||||
their domain management privileges if they already have that role assigned. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
||||
if you want to remove the user from a domain.
|
||||
If you cancel the domain invitation here, it won't trigger any email notifications.
|
||||
It also won't remove the user's domain management privileges if they already logged in. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">
|
||||
User Domain Roles
|
||||
</a>
|
||||
table if you want to remove their domain management privileges.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the domain invitation here, it won't trigger any emails. It also won't remove
|
||||
their domain management privileges if they already have that role assigned. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
||||
if you want to remove the user from a domain.
|
||||
If you cancel the domain invitation here, it won't trigger any email notifications.
|
||||
It also won't remove the user's domain management privileges if they already logged in. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">
|
||||
User Domain Roles
|
||||
</a>
|
||||
table if you want to remove their domain management privileges.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
{% if show_formatted_name %}
|
||||
{% if user.get_formatted_name %}
|
||||
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ user.get_formatted_name }}
|
||||
{% else %}
|
||||
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<p>
|
||||
Domain invitations contain all individuals who have been invited to manage a .gov domain.
|
||||
Invitations are sent via email, and the recipient must log in to the registrar to officially
|
||||
accept and become a domain manager.
|
||||
This table contains all individuals who have been invited to manage a .gov domain.
|
||||
These individuals must log in to the registrar to officially accept and become a domain manager.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with an "invited" status will prevent the user from signing in.
|
||||
A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will not revoke that user's access from the domain. To remove a user who has already signed in, go to <a class="text-underline" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User domain roles</a> and delete the role for the correct domain/manager combination.
|
||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent.
|
||||
A “received” status indicates that the recipient has logged in.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If an invitation is created in this table, an email will not be sent.
|
||||
To have an email sent, go to the domain in <a class="text-underline" href="{% url 'admin:registrar_domain_changelist' %}">Domains</a>,
|
||||
click the “Manage domain” button, and add a domain manager.
|
||||
If you invite someone to a domain by using this table, they’ll receive an email notification.
|
||||
The existing managers of the domain will also be notified. However, canceling an invitation here won’t trigger any emails.
|
||||
</p>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<p>
|
||||
Portfolio invitations contain all individuals who have been invited to become members of an organization.
|
||||
Invitations are sent via email, and the recipient must log in to the registrar to officially
|
||||
accept and become a member.
|
||||
This table contains all individuals who have been invited to become members of a portfolio.
|
||||
These individuals must log in to the registrar to officially accept and become a member.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent
|
||||
or that the recipient has logged in but is already a member of an organization.
|
||||
A “received” status indicates that the recipient has logged in.
|
||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation
|
||||
was sent or that the recipient has logged in but is already a member of another portfolio. A “received”
|
||||
status indicates that the recipient has logged in.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you invite someone to a portfolio by using this table, they’ll receive an email notification.
|
||||
If you assign them "admin" access, the existing portfolio admins will also be notified. However, canceling an invitation here won’t trigger any emails.
|
||||
</p>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<p>
|
||||
This table represents the managers who are assigned to each domain in the registrar.
|
||||
There are separate records for each domain/manager combination.
|
||||
Managers can update information related to a domain, such as DNS data and security contact.
|
||||
This table represents the managers who are assigned to each domain in the registrar. There are separate records for each domain/manager combination.
|
||||
Managers can update information related to a domain, such as DNS data and security contact.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The creator of an approved domain request automatically becomes a manager for that domain.
|
||||
Anyone who retrieves a domain invitation is also assigned the manager role.
|
||||
The creator of an approved domain request automatically becomes a manager for that domain.
|
||||
Anyone who retrieves a domain invitation will also appear in this table as a manager.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you add or remove someone to a domain by using this table, those actions won’t trigger notification emails.
|
||||
</p>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<p>
|
||||
This table represents the members of each portfolio in the registrar. There are separate records for each member/portfolio combination.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Each member is assigned one of two access levels: admin or basic. Only admins can manage member permissions and organization metadata.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you add or remove someone to a portfolio by using this table, those actions won’t trigger notification emails.
|
||||
</p>
|
|
@ -69,7 +69,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% elif field.field.name == "portfolio_senior_official" %}
|
||||
<div class="readonly">
|
||||
{% if original_object.portfolio.senior_official %}
|
||||
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ field.contents|striptags }}
|
||||
{% else %}
|
||||
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No senior official found.<br>
|
||||
{% endif %}
|
||||
|
@ -78,7 +82,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% if all_contacts.count > 2 %}
|
||||
<div class="readonly">
|
||||
{% for contact in all_contacts %}
|
||||
<a href="{% url 'admin:registrar_contact_change' contact.id %}">{{ contact.get_formatted_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'admin:registrar_contact_change' contact.id %}">{{ contact.get_formatted_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -153,6 +161,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
<p>No additional members found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% elif field.field.name == "senior_official" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% else %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% endif %}
|
||||
|
@ -160,6 +172,16 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% endblock field_readonly %}
|
||||
|
||||
{% block field_other %}
|
||||
{% comment %}
|
||||
.gov override - add Aria messages for select2 dropdowns. These messages are hooked-up to their respective DOM
|
||||
elements via javascript (see andi.js)
|
||||
{% endcomment %}
|
||||
{% if "related_widget_wrapper" in field.field.field.widget.template_name %}
|
||||
<span id="{{ field.field.id_for_label }}--aria-description" class="visually-hidden admin-select--aria-description">
|
||||
{{ field.field.label }}, edit, has autocomplete. To set the value, use the arrow keys or type the text.
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if field.field.name == "action_needed_reason_email" %}
|
||||
{{ field.field }}
|
||||
|
||||
|
@ -251,7 +273,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
|
||||
{% elif field.field.name == "rejection_reason_email" %}
|
||||
{{ field.field }}
|
||||
|
||||
<div class="margin-top-05 text-faded custom-email-placeholder">
|
||||
–
|
||||
</div>
|
||||
|
@ -331,7 +352,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if original_object.rejection_reason_email %}
|
||||
<input id="last-sent-rejection-email-content" class="display-none" value="{{original_object.rejection_reason_email}}">
|
||||
{% else %}
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
{% for admin in admins %}
|
||||
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
|
||||
<tr>
|
||||
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||
{% if adminform.form.is_omb_analyst %}
|
||||
<td>{{ admin.user.get_formatted_name }}</td>
|
||||
{% else %}
|
||||
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||
{% endif %}
|
||||
<td>{{ admin.user.title }}</td>
|
||||
<td>
|
||||
{% if admin.user.email %}
|
||||
|
|
|
@ -30,6 +30,9 @@
|
|||
<a href={{ url }}>No senior official found. Create one now.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% else %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you add someone to a portfolio here, it will trigger an invitation email when you click "save." If you don't want to trigger an email, use the <a class="usa-link" href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">User portfolio permissions table</a> instead.
|
||||
If you invite someone to a portfolio here, it will trigger email notifications. If you don't want to trigger emails, use the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||
User Portfolio Permissions
|
||||
</a>
|
||||
table instead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the portfolio invitation here, it won't trigger any emails. It also won't remove the user's
|
||||
portfolio access if they already logged in. Go to the
|
||||
If you cancel the portfolio invitation here, it won't trigger any email notifications.
|
||||
It also won't remove the user's portfolio access if they already logged in. Go to the
|
||||
<a href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||
User Portfolio Permissions
|
||||
</a>
|
||||
table if you want to remove the user from a portfolio.
|
||||
table if you want to remove their portfolio access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "admin/delete_selected_confirmation.html" %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the portfolio invitation here, it won't trigger any email notifications.
|
||||
It also won't remove the user's portfolio access if they already logged in. Go to the
|
||||
<a href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||
User Portfolio Permissions
|
||||
</a>
|
||||
table if you want to remove their portfolio access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -6,7 +6,10 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you add someone to a domain here, it will not trigger any emails. To trigger emails, use the <a class="usa-link" href="{% url 'admin:registrar_domaininvitation_changelist' %}">User Domain Role invitations table</a> instead.
|
||||
If you add someone to a domain here, it won't trigger any email notifications. To trigger emails, use the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_domaininvitation_changelist' %}">
|
||||
Domain Invitations
|
||||
</a> table instead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a domain here, it won't trigger any emails when you click "save."
|
||||
If you remove someone from a domain here, it won't trigger any email notifications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a domain here, it won't trigger any emails when you click "save."
|
||||
If you remove someone from a domain here, it won't trigger any email notifications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you add someone to a portfolio here, it will not trigger an invitation email. To trigger an email, use the <a class="usa-link" href="{% url 'admin:registrar_portfolioinvitation_changelist' %}">Portfolio invitations table</a> instead.
|
||||
If you add someone to a portfolio here, it won't trigger any email notifications. To trigger emails, use the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_portfolioinvitation_changelist' %}">
|
||||
Portfolio Invitations
|
||||
</a>
|
||||
table instead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a portfolio here, it will not send any emails when you click "Save".
|
||||
If you remove someone from a portfolio here, it won't trigger any email notifications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "admin/delete_selected_confirmation.html" %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a portfolio here, it won't trigger any email notifications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -46,7 +46,7 @@
|
|||
{# messages block is under the back breadcrumb link #}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@
|
|||
{% if request.path|endswith:"renewal"%}
|
||||
<h1>Renew {{domain.name}} </h1>
|
||||
{%else%}
|
||||
<h1 class="break-word">Domain Overview</h1>
|
||||
<h1 class="break-word">Domain overview</h1>
|
||||
{% endif%}
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
{% if domain.dnssecdata is not None %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not enabled' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio %}
|
||||
|
|
|
@ -5,10 +5,21 @@
|
|||
|
||||
{% block domain_content %}
|
||||
|
||||
{# this is right after the messages block in the parent template #}
|
||||
{# this is right after the messages block in the parent template. #}
|
||||
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
{% if formset.initial|length >= formset.max_num %}
|
||||
<div class="usa-alert usa-alert--do-not-reset usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__text">
|
||||
You’ve reached the maximum amount of allowed name server records (13).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{% if portfolio %}
|
||||
|
@ -32,82 +43,286 @@
|
|||
{% endif %}
|
||||
{% endblock breadcrumb %}
|
||||
|
||||
<h1>DNS name servers</h1>
|
||||
|
||||
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
||||
|
||||
<p>Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You must add at least two name servers (13 max).</p>
|
||||
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<p class="margin-top-0">Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is “example.gov” and your name server is “ns1.example.gov,” then an IP address is required). Multiple IP addresses must be separated with commas.</p>
|
||||
<p class="margin-bottom-0">This step is uncommon unless you self-host your DNS or use custom addresses for your nameserver.</p>
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-6">
|
||||
<h1 class="tablet:margin-bottom-1">Name servers</h1>
|
||||
</div>
|
||||
|
||||
<div class="tablet:grid-col-6 text-right--tablet">
|
||||
<button type="button" class="usa-button margin-bottom-1 tablet:float-right" id="nameserver-add-button">
|
||||
Add name servers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
<p>Add a name server record by clicking “Add name servers.” You must add at least two name servers (13 max).</p>
|
||||
|
||||
<form class="usa-form usa-form--extra-large nameservers-form" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
{% comment %}
|
||||
This template supports the rendering of three different types of nameserver forms, conditionally displayed:
|
||||
1 - Add New Namervers form (rendered when there are no existing nameservers defined for the domain)
|
||||
2 - Nameserver table (rendered when the domain has existing nameservers, which can be viewed and edited)
|
||||
3 - Add New Nameserver (rendered above the Nameserver table to add a single additional nameserver)
|
||||
{% endcomment %}
|
||||
|
||||
{% for form in formset %}
|
||||
<div class="repeatable-form">
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-5">
|
||||
{{ form.domain }}
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||
{% if forloop.counter <= 2 %}
|
||||
{# span_for_text will wrap the copy in s <span>, which we'll use in the JS for this component #}
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" span_for_text=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with span_for_text=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% if formset.initial and formset.forms.0.initial %}
|
||||
|
||||
{% comment %}This section renders both the Nameserver table and the Add New Nameserver {% endcomment %}
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
{% for form in formset %}
|
||||
{% if forloop.last and not form.initial %}
|
||||
|
||||
{% comment %}
|
||||
This section renders the Add New Nameserver form.
|
||||
This section does not render if the last form has initial data (this occurs if 13 nameservers already exist)
|
||||
{% endcomment %}
|
||||
|
||||
<section class="add-nameservers-form display-none section-outlined">
|
||||
{{ form.domain }}
|
||||
<h2>Add a name server</h2>
|
||||
<div class="repeatable-form">
|
||||
<div class="grid-row grid-gap-2 flex-end minh-143px">
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||
{% with attr_required=True span_for_text=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with attr_required=True add_initial_value_attr=True label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="margin-top-2">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--outline nameserver-cancel-add-form"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the name server form to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked" id="nameserver-table">
|
||||
<caption class="sr-only">Your registered domains</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" role="columnheader">Name servers</th>
|
||||
<th scope="col" role="columnheader"><span class="sr-only">IP address</span></th>
|
||||
<th scope="col" role="columnheader" class="width-0 padding-right-0">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in formset %}
|
||||
{% if not forloop.last or form.initial %}
|
||||
|
||||
{% comment %}
|
||||
This section renders table rows for each existing nameserver. Two rows are rendered, a readonly row
|
||||
and an edit row. Only one of which is displayed at a time.
|
||||
{% endcomment %}
|
||||
|
||||
{{ form.domain }}
|
||||
<!-- Readonly row -->
|
||||
<tr>
|
||||
<td colspan="2" aria-colspan="2">{{ form.server.value }} {% if form.ip.value %}({{ form.ip.value }}){% endif %}</td>
|
||||
<td class="padding-right-0">
|
||||
<div class="tablet:display-flex tablet:flex-row">
|
||||
<button type="button" class='usa-button usa-button--unstyled margin-right-2 margin-top-0 nameserver-edit'>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#edit"></use>
|
||||
</svg>
|
||||
Edit <span class="usa-sr-only">{{ form.server.value }}</span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
role="button"
|
||||
id="button-trigger-delete-{{ form.server.value }}"
|
||||
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary visible-mobile-flex nameserver-delete-kebab"
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||
</svg>
|
||||
Delete
|
||||
</a>
|
||||
|
||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
||||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions margin-top-0"
|
||||
aria-expanded="false"
|
||||
aria-controls="more-actions-{{ form.server.value }}"
|
||||
aria-label="More Actions for ({{ form.server.value }})"
|
||||
>
|
||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="more-actions-{{ form.server.value }}" class="usa-accordion__content usa-prose shadow-1 left-auto right-neg-1" hidden>
|
||||
<h2>More options</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary nameserver-delete-kebab"
|
||||
name="btn-delete-kebab-click"
|
||||
aria-label="Delete the name server from the registry"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Edit row -->
|
||||
<tr class="edit-row display-none">
|
||||
<td class="text-bottom">
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||
{% with attr_required=True add_initial_value_attr=True span_for_text=True add_group_class="usa-form-group--unstyled-error margin-top-0" %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="text-bottom">
|
||||
{% with attr_required=True add_initial_value_attr=True label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label add_group_class="usa-form-group--unstyled-error margin-top-0" %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="padding-right-0 text-bottom">
|
||||
<button class="usa-button usa-button--unstyled display-block margin-top-1" type="submit">Save</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled display-block nameserver-cancel"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the name server form to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled display-block text-secondary nameserver-delete"
|
||||
name="btn-delete-click"
|
||||
aria-label="Delete the name server from the registry"
|
||||
>Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-5">
|
||||
{% with label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-2">
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075 text-secondary line-height-sans-5">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>Delete
|
||||
<span class="sr-only">Name server {{forloop.counter}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg>Add another name server
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% comment %} Work around USWDS' button margins to add some spacing between the submit and the 'add more'
|
||||
This solution still works when we remove the 'add more' at 13 forms {% endcomment %}
|
||||
<div class="margin-top-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--outline"
|
||||
{% comment %}
|
||||
This section renders Add New Nameservers form which renders when there are no existing
|
||||
nameservers defined on the domain.
|
||||
{% endcomment %}
|
||||
|
||||
<section class="add-nameservers-form display-none section-outlined">
|
||||
{% include "includes/required_fields.html" %}
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate>
|
||||
<h2>Add name servers</h2>
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
{% for form in formset %}
|
||||
{{ form.domain }}
|
||||
<div class="repeatable-form">
|
||||
<div class="grid-row grid-gap-2 flex-end minh-143px">
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% if forloop.counter <= 2 %}
|
||||
{# span_for_text will wrap the copy in s <span>, which we'll use in the JS for this component #}
|
||||
{% with attr_required=True add_initial_value_attr=True span_for_text=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with span_for_text=True add_initial_value_attr=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with attr_required=True add_initial_value_attr=True label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="margin-top-2">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--outline nameserver-cancel-add-form"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the name server form to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
id="unsaved_changes_trigger"
|
||||
href="#unsaved-changes-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="unsaved-changes-modal"
|
||||
data-open-modal
|
||||
>Trigger unsaved changes modal</a>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="unsaved-changes-modal"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="You have unsaved changes that will be lost."
|
||||
>
|
||||
{% 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" %}
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="delete_trigger"
|
||||
href="#delete-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="delete-modal"
|
||||
data-open-modal
|
||||
>Trigger delete modal</a>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="delete-modal"
|
||||
aria-labelledby="Are you sure you want to delete this name server?"
|
||||
aria-describedby="This will delete the name server from your DNS records. This action cannot be undone."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this name server?" modal_description="This will delete the name server from your DNS records. This action cannot be undone." modal_button_id="delete-click-button" modal_button_text="Yes, delete name server" modal_button_class="usa-button--secondary" %}
|
||||
</div>
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<!-- Banner for if_policy_acknowledged -->
|
||||
{% if form.is_policy_acknowledged.errors %}
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
{% for error in form.is_policy_acknowledged.errors %}
|
||||
<p class="usa-alert__text">{{ error }}</p>
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">our naming requirements</a>. Your domain name must:
|
||||
<p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% if requires_feb_questions %}https://get.gov/domains/executive-branch-guidance/{% else %}{% public_site_url 'domains/choosing' %}{% endif %}">our naming requirements</a>. Your domain name must:
|
||||
<ul class="usa-list">
|
||||
<li>Be available </li>
|
||||
<li>Relate to your organization’s name, location, and/or services </li>
|
||||
<li>Relate to your organization's name, location, and/or services </li>
|
||||
<li>Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience) </li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
|
||||
{% if not is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}</p>
|
||||
{% if not is_federal %}In most instances, this requires including your state's two-letter abbreviation.{% endif %}</p>
|
||||
|
||||
{% if not portfolio %}
|
||||
<p>Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
||||
<p>Requests for your organization's initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>Note that <strong>only federal agencies can request generic terms</strong> like
|
||||
|
@ -41,9 +41,10 @@
|
|||
<legend>
|
||||
<h2>What .gov domain do you want?</h2>
|
||||
</legend>
|
||||
|
||||
<p id="domain_instructions" class="margin-top-05">After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all our requirements after you complete the rest of this form.</p>
|
||||
|
||||
<p id="domain_instructions" class="margin-top-05">
|
||||
After you enter your domain, we'll make sure it's available and that it meets some of our naming requirements.
|
||||
If your domain passes these initial checks, we'll verify that it meets all our requirements after you complete the rest of this form.
|
||||
</p>
|
||||
{% with attr_aria_labelledby="domain_instructions domain_instructions2" attr_aria_describedby="id_dotgov_domain-requested_domain--toast" %}
|
||||
{# attr_validate / validate="domain" invokes code in getgov.min.js #}
|
||||
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
||||
|
@ -63,10 +64,9 @@
|
|||
<legend>
|
||||
<h2 id="alternative-domains-title">Alternative domains (optional)</h2>
|
||||
</legend>
|
||||
|
||||
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
||||
you your first choice?</p>
|
||||
|
||||
<p id="alt_domain_instructions" class="margin-top-05">
|
||||
Are there other domains you'd like if we can't give you your first choice?
|
||||
</p>
|
||||
{% with attr_aria_labelledby="alt_domain_instructions" %}
|
||||
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
|
||||
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
|
||||
|
@ -83,10 +83,10 @@
|
|||
<div class="usa-sr-only" id="alternative-domains__add-another-alternative">Add another alternative domain</div>
|
||||
<button aria-labelledby="alternative-domains-title" aria-describedby="alternative-domains__add-another-alternative" type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add another alternative</span>
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#add_circle"></use>
|
||||
</svg>
|
||||
<span class="margin-left-05">Add another alternative</span>
|
||||
</button>
|
||||
|
||||
<div class="margin-bottom-3">
|
||||
<div class="usa-sr-only" id="alternative-domains__check-availability">Check domain availability</div>
|
||||
<button
|
||||
|
@ -98,10 +98,41 @@
|
|||
aria-describedby="alternative-domains__check-availability"
|
||||
>Check availability</button>
|
||||
</div>
|
||||
|
||||
<p class="margin-top-05">
|
||||
If you're not sure this is the domain you want, that's ok. You can change the domain later.
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<p class="margin-top-05">If you’re not sure this is the domain you want, that’s ok. You can change the domain later. </p>
|
||||
{{ forms.2.management_form }}
|
||||
{{ forms.3.management_form }}
|
||||
|
||||
</fieldset>
|
||||
{% if requires_feb_questions %}
|
||||
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form">
|
||||
<legend>
|
||||
<h2>Does this submission meet each domain naming requirement?</h2>
|
||||
</legend>
|
||||
<p id="dotgov-domain-naming-requirements" class="margin-top-05">
|
||||
OMB will review each request against the domain
|
||||
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="https://get.gov/domains/executive-branch-guidance/">
|
||||
naming requirements for executive branch agencies
|
||||
</a>.
|
||||
Agency submissions are expected to meet each requirement.
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.2.feb_naming_requirements %}
|
||||
{% endwith %}
|
||||
|
||||
{# Conditional Details Field – only shown when the executive naming requirements radio is "False" #}
|
||||
<div id="domain-naming-requirements-details-container" class="conditional-panel" style="display: none;">
|
||||
<p class="usa-label">
|
||||
Provide details below <span class="usa-label--required">*</span>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %}
|
||||
{% input_with_errors forms.3.feb_naming_requirements_details %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,17 +3,84 @@
|
|||
|
||||
{% block form_instructions %}
|
||||
<p>.Gov domains are intended for public use. Domains will not be given to organizations that only want to reserve a domain name (defensive registration) or that only intend to use the domain internally (as for an intranet).</p>
|
||||
<p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
|
||||
<h2>What is the purpose of your requested domain?</h2>
|
||||
<p>Describe how you’ll use your .gov domain. Will it be used for a website, email, or something else?</p>
|
||||
<p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# commented out so it does not appear on this page #}
|
||||
{# empty this block so it doesn't show on this page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% if requires_feb_questions %}
|
||||
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form">
|
||||
{{forms.0.management_form}}
|
||||
{{forms.1.management_form}}
|
||||
{{forms.2.management_form}}
|
||||
{{forms.3.management_form}}
|
||||
{{forms.4.management_form}}
|
||||
{{forms.5.management_form}}
|
||||
<h2>What is the purpose of your requested domain?</h2>
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.feb_purpose_choice %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="purpose-details-container" class="conditional-panel display-none">
|
||||
<p class="usa-label">
|
||||
<em>Provide details below <span class="usa-label--required">*</span></em>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
|
||||
{% input_with_errors forms.1.purpose %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
|
||||
<h2>Do you have a target time frame for launching this domain?</h2>
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.2.has_timeframe %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="purpose-timeframe-details-container" class="conditional-panel">
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
|
||||
{% input_with_errors forms.3.time_frame_details %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
|
||||
<h2>Will the domain name be used for an interagency initiative?</h2>
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.4.is_interagency_initiative %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="purpose-interagency-initaitive-details-container" class="conditional-panel">
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
|
||||
{% input_with_errors forms.5.interagency_initiative_details %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<h2>What is the purpose of your requested domain?</h2>
|
||||
<p>Describe how you’ll use your .gov domain. Will it be used for a website, email, or something else?</p>
|
||||
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.purpose %}
|
||||
{% input_with_errors forms.1.purpose %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov"%}Add security email{% else %}Save{% endif %}</button>
|
||||
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov" or form.security_email.value == "help@get.gov"%}Add security email{% else %}Save{% endif %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -5,7 +5,7 @@ A domain manager was removed from {{ domain.name }}.
|
|||
|
||||
REMOVED BY: {{ removed_by.email }}
|
||||
REMOVED ON: {{ date }}
|
||||
MANAGER REMOVED: {{ manager_removed.email }}
|
||||
MANAGER REMOVED: {{ manager_removed_email }}
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %}
|
||||
|
||||
An update was made to your .gov organization.
|
||||
|
||||
ORGANIZATION: {{ portfolio }}
|
||||
UPDATED BY: {{ editor.email }}
|
||||
UPDATED ON: {{ date }}
|
||||
INFORMATION UPDATED: {{ updated_info }}
|
||||
|
||||
You can view this update in the .gov registrar <https://manage.get.gov>.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
WHY DID YOU RECEIVE THIS EMAIL?
|
||||
You're listed as an admin for {{ portfolio }}, so you'll receive a
|
||||
notification whenever changes are made to that .gov organization.
|
||||
|
||||
If you have questions or concerns, reach out to the person who made the change or reply
|
||||
to this email.
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov
|
||||
domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency
|
||||
(CISA) <https://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
An update was made to your .gov organization
|
|
@ -57,7 +57,7 @@ THANK YOU
|
|||
The .gov team
|
||||
|
||||
.Gov blog <https://get.gov/updates/>
|
||||
Domain management <{{ manage_url }}}>
|
||||
Domain management <{{ manage_url }}>
|
||||
Get.gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
|
||||
|
|
|
@ -15,14 +15,14 @@
|
|||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<h1>Manage your domains</h1>
|
||||
|
||||
<p class="margin-top-4">
|
||||
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
|
||||
>
|
||||
Start a new domain request
|
||||
</button>
|
||||
</p>
|
||||
<div class="grid-row margin-bottom-3">
|
||||
<h1 class="flex-fill">Manage your domains</h1>
|
||||
<div>
|
||||
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link">
|
||||
Start a new domain request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
|
||||
{% include "includes/domain_requests_table.html" %}
|
||||
|
|
|
@ -4,49 +4,20 @@
|
|||
{% url 'get_domain_requests_json' as url %}
|
||||
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
||||
|
||||
<section class="section-outlined domain-requests {% if portfolio %}section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %}section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||
<section class="section-outlined domain-requests {% if portfolio %}margin-top-0 section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
{% if not portfolio %}
|
||||
<h2 id="domain-requests-header" class="display-inline-block">Domain requests</h2>
|
||||
<div class="grid-row grid-col-12">
|
||||
<h2 id="domain-requests-header" class="display-inline-block flex-fill">Domain requests</h2>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||
{% endif %}
|
||||
|
||||
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||
<section aria-label="Domain requests search component" id="domain-requests-search-component" class="margin-top-2">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domain-requests__reset-search" type="button" aria-labelledby="domain-requests-search-component">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domain-requests__search-field"
|
||||
type="search"
|
||||
name="domain-requests-search"
|
||||
{% if portfolio %}
|
||||
placeholder="Search by domain name or creator"
|
||||
{% else %}
|
||||
placeholder="Search by domain name"
|
||||
{% endif %}
|
||||
aria-labelledby="domain-requests-search-component"
|
||||
/>
|
||||
<div class="usa-sr-only" id="domain-requests-search-button__description">Click to search</div>
|
||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit" aria-labelledby="domain-requests-search-component" aria-describedby="domain-requests-search-button__description">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
{% with label_text=portfolio|yesno:"Search by domain name or creator,Search by domain name" item_name="domain-requests" aria_label_text="Domain requests search component"%}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% if portfolio %}
|
||||
|
|
|
@ -26,56 +26,32 @@
|
|||
{% endif %}
|
||||
|
||||
<section class="section-outlined domains margin-top-0{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domains">
|
||||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
{% if not portfolio %}
|
||||
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
||||
<div class="grid-row grid-col-12">
|
||||
<h2 id="domains-header" class="display-inline-block flex-fill">Domains</h2>
|
||||
<!-- ---------- EXPORT (non-org placement) ---------- -->
|
||||
{% if user_domain_count and user_domain_count > 0 %}
|
||||
{% with export_aria="Domains report component" export_url='export_data_type_user' %}
|
||||
{% include "includes/export.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||
{% endif %}
|
||||
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||
<section aria-label="Domains search component" class="margin-top-2" id="domains-search-component">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domains__reset-search" type="button" aria-labelledby="domains-search-component">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domains__search-field"
|
||||
type="search"
|
||||
name="domains-search"
|
||||
placeholder="Search by domain name"
|
||||
aria-labelledby="domains-search-component"
|
||||
/>
|
||||
<div class="usa-sr-only" id="domains-search-button__description">Click to search</div>
|
||||
<button class="usa-button" type="submit" id="domains__search-field-submit" aria-labelledby="domains-search-component" aria-describedby="domains-search-button__description">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% if user_domain_count and user_domain_count > 0 %}
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||
<section aria-label="Domains report component" class="margin-top-205" id="domains-report-component">
|
||||
<div class="usa-sr-only" id="domains-export-button__description">Click to export as csv</div>
|
||||
<button data-href="{% url 'export_data_type_user' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="domains-report-component" aria-describedby="domains-export-button__description">
|
||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg>Export as CSV
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
{% with label_text="Search by domain name" item_name="domains" aria_label_text="Domains search component"%}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
<!-- ---------- EXPORT (org placement) ---------- -->
|
||||
{% if user_domain_count and user_domain_count > 0 and portfolio%}
|
||||
{% with export_aria="Domains report component" export_url='export_data_type_user' %}
|
||||
{% include "includes/export.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Non org model banner -->
|
||||
{% if num_expiring_domains > 0 and not portfolio %}
|
||||
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||
|
|
12
src/registrar/templates/includes/export.html
Normal file
12
src/registrar/templates/includes/export.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% load static %}
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} flex-auto desktop:padding-left-3{% endif %} margin-top-0">
|
||||
<section aria-label="{{export_aria}}" class="margin-top-205">
|
||||
<button data-href="{% url export_url %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right use-button-as-link">
|
||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg>Export as CSV
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{% if form.errors %}
|
||||
<div id="form-errors">
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert" tabindex="0">
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<span class="usa-sr-only">Error:</span>
|
||||
{{ error|escape }}
|
||||
|
@ -10,7 +10,7 @@
|
|||
{% endfor %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" tabindex="0">
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<span class="usa-sr-only">Error:</span>
|
||||
{{ error|escape }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body {% if no_max_width %} maxw-none {% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
|
|
@ -91,9 +91,9 @@
|
|||
aria-describedby="You have unsaved changes that will be lost."
|
||||
>
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member-domains' pk=portfolio_permission.id as url %}
|
||||
{% url 'member-domains' member_pk=portfolio_permission.id as url %}
|
||||
{% else %}
|
||||
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url %}
|
||||
{% url 'invitedmember-domains' invitedmember_pk=portfolio_invitation.id as url %}
|
||||
{% endif %}
|
||||
|
||||
{% 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_url=url modal_button_text="Continue without saving" %}
|
||||
|
|
|
@ -7,47 +7,14 @@
|
|||
<span id="get_members_json_url" class="display-none">{{url}}</span>
|
||||
<section class="section-outlined members margin-top-0 section-outlined--border-base-light" id="members">
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6 section-outlined__search--widescreen">
|
||||
<section aria-label="Members search component" class="margin-top-2" id="members-search-component">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="members__reset-search" type="button" aria-labelledby="members-search-component">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="members__search-field"
|
||||
type="search"
|
||||
name="members-search"
|
||||
placeholder="Search by member name"
|
||||
aria-labelledby="members-search-component"
|
||||
/>
|
||||
<div class="usa-sr-only" id="members-search-button__description">Click to search</div>
|
||||
<button class="usa-button" type="submit" id="members__search-field-submit" aria-labelledby="members-search-component" aria-describedby="members-search-button__description">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||
<section aria-label="Members report component" class="margin-top-205" id="members-report-component">
|
||||
<div class="usa-sr-only" id="members-export-button__description">Click to export as csv</div>
|
||||
<button href="{% url 'export_members_portfolio' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="members-report-component" aria-describedby="members-export-button__description">
|
||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg>Export as CSV
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
{% with label_text="Search by member name" item_name="members" aria_label_text="Members search component"%}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
{% with export_aria="Members report component" export_url='export_members_portfolio' %}
|
||||
{% include "includes/export.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- ---------- MAIN TABLE ---------- -->
|
||||
<div class="display-none margin-top-0" id="members__table-wrapper">
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
{% if cancel_button_text %}{{ cancel_button_text }}{% else %}Cancel{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
{% load static %}
|
||||
|
||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||
<div class="section-outlined__search tablet:grid-col">
|
||||
<section aria-label="{{aria_label_text}}">
|
||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||
<form class="usa-search {% if use_search_icon %} usa-search--small {% else %} usa-search--default {% endif %}usa-search--show-label" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<label class="usa-label display-block margin-bottom-05 maxw-none" for="{{item_name}}__search-field">
|
||||
<label id="{{item_name}}__search-label" class="usa-label display-block maxw-none margin-top-2 margin-bottom-1" for="{{item_name}}__search-field">
|
||||
{{ label_text }}
|
||||
</label>
|
||||
<div class="usa-search--show-label__input-wrapper flex-align-self-end">
|
||||
</label>
|
||||
<div class="usa-search--show-label__input-wrapper">
|
||||
<input
|
||||
class="usa-input minw-15"
|
||||
id="{{item_name}}__search-field"
|
||||
type="search"
|
||||
name="{{item_name}}-search"
|
||||
/>
|
||||
<button class="usa-button" type="submit" id="{{item_name}}__search-field-submit">
|
||||
<span class="usa-search__submit-text">Search </span>
|
||||
<button class="usa-button" type="submit" id="{{item_name}}__search-field-submit" aria-labelledby="{{item_name}}__search-label">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
{% if not use_search_icon %}
|
||||
<span class="usa-search__submit-text">Search </span>
|
||||
{% endif %}
|
||||
</button>
|
||||
<button class="usa-button usa-button--unstyled margin-left-3 display-none flex-1" id="{{item_name}}__reset-search" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
|
||||
{% url 'members' as url %}
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains-edit' pk=portfolio_permission.id as url3 %}
|
||||
{% url 'member' member_pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains-edit' member_pk=portfolio_permission.id as url3 %}
|
||||
{% else %}
|
||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains-edit' pk=portfolio_invitation.id as url3 %}
|
||||
{% url 'invitedmember' invitedmember_pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains-edit' invitedmember_pk=portfolio_invitation.id as url3 %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
|
||||
{% url 'members' as url %}
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains' pk=portfolio_permission.id as url3 %}
|
||||
{% url 'member' member_pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains' member_pk=portfolio_permission.id as url3 %}
|
||||
{% else %}
|
||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
||||
{% url 'invitedmember' invitedmember_pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains' invitedmember_pk=portfolio_invitation.id as url3 %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
<!-- Navigation breadcrumbs -->
|
||||
{% url 'members' as url %}
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member' member_pk=portfolio_permission.id as url2 %}
|
||||
{% else %}
|
||||
{% url 'invitedmember' pk=invitation.id as url2 %}
|
||||
{% url 'invitedmember' invitedmember_pk=invitation.id as url2 %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0 bg-gray-1" aria-label="Portfolio member breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
|
|
|
@ -16,15 +16,14 @@
|
|||
{% endblock messages%}
|
||||
|
||||
<div id="main-content">
|
||||
<h1 id="domain-requests-header" class="margin-bottom-1">Domain requests</h1>
|
||||
<div class="grid-row grid-gap">
|
||||
|
||||
<div class="grid-row grid-gap margin-bottom-3">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
<h1 id="domain-requests-header" class="margin-bottom-1">Domain requests</h1>
|
||||
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||
</div>
|
||||
|
||||
{% if has_edit_request_portfolio_permission %}
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||
</div>
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
|
||||
<p class="float-right-tablet tablet:margin-y-0">
|
||||
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
|
||||
>
|
||||
|
@ -32,10 +31,9 @@
|
|||
</button>
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
||||
|
|
|
@ -16,7 +16,7 @@ Edit your User Profile |
|
|||
<div class="desktop:grid-col-8 desktop:grid-offset-2">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
|
|
@ -173,6 +173,10 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
if aria_labels:
|
||||
context["aria_label"] = " ".join(aria_labels)
|
||||
|
||||
# Conditionally add the data-initial-value attribute
|
||||
if context.get("add_initial_value_attr", False):
|
||||
attrs["data-initial-value"] = field.initial or ""
|
||||
|
||||
# ask Django to give us the widget dict
|
||||
# see Widget.get_context() on
|
||||
# https://docs.djangoproject.com/en/4.1/ref/forms/widgets
|
||||
|
|
|
@ -1010,6 +1010,27 @@ def create_user(**kwargs):
|
|||
return user
|
||||
|
||||
|
||||
def create_omb_analyst_user(**kwargs):
|
||||
"""Creates a analyst user with is_staff=True and the group cisa_analysts_group"""
|
||||
User = get_user_model()
|
||||
p = "userpass"
|
||||
user = User.objects.create_user(
|
||||
username=kwargs.get("username", "ombanalystuser"),
|
||||
email=kwargs.get("email", "ombanalyst@example.com"),
|
||||
first_name=kwargs.get("first_name", "first"),
|
||||
last_name=kwargs.get("last_name", "last"),
|
||||
is_staff=kwargs.get("is_staff", True),
|
||||
title=kwargs.get("title", "title"),
|
||||
password=kwargs.get("password", p),
|
||||
phone=kwargs.get("phone", "8003111234"),
|
||||
)
|
||||
# Retrieve the group or create it if it doesn't exist
|
||||
group, _ = UserGroup.objects.get_or_create(name="omb_analysts_group")
|
||||
# Add the user to the group
|
||||
user.groups.set([group])
|
||||
return user
|
||||
|
||||
|
||||
def create_test_user():
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
|
@ -1423,10 +1444,8 @@ class MockEppLib(TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData(
|
||||
"defaultTech", "dotgov@cisa.dhs.gov"
|
||||
)
|
||||
mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultSec", "dotgov@cisa.dhs.gov")
|
||||
mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultTech", "help@get.gov")
|
||||
mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultSec", "help@get.gov")
|
||||
mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("securityContact", "security@mail.gov")
|
||||
mockTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData("technicalContact", "tech@mail.gov")
|
||||
mockAdministrativeContact = InfoDomainWithContacts.dummyInfoContactResultData("adminContact", "admin@mail.gov")
|
||||
|
@ -1449,7 +1468,7 @@ class MockEppLib(TestCase):
|
|||
)
|
||||
|
||||
infoDomainThreeHosts = fakedEppObject(
|
||||
"my-nameserver.gov",
|
||||
"threenameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
|
@ -1460,7 +1479,7 @@ class MockEppLib(TestCase):
|
|||
)
|
||||
|
||||
infoDomainFourHosts = fakedEppObject(
|
||||
"fournameserversDomain.gov",
|
||||
"fournameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
|
@ -1471,6 +1490,47 @@ class MockEppLib(TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
infoDomainTwelveHosts = fakedEppObject(
|
||||
"twelvenameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
"ns1.my-nameserver-1.com",
|
||||
"ns1.my-nameserver-2.com",
|
||||
"ns1.cats-are-superior3.com",
|
||||
"ns1.explosive-chicken-nuggets.com",
|
||||
"ns5.example.com",
|
||||
"ns6.example.com",
|
||||
"ns7.example.com",
|
||||
"ns8.example.com",
|
||||
"ns9.example.com",
|
||||
"ns10.example.com",
|
||||
"ns11.example.com",
|
||||
"ns12.example.com",
|
||||
],
|
||||
)
|
||||
|
||||
infoDomainThirteenHosts = fakedEppObject(
|
||||
"thirteennameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
"ns1.my-nameserver-1.com",
|
||||
"ns1.my-nameserver-2.com",
|
||||
"ns1.cats-are-superior3.com",
|
||||
"ns1.explosive-chicken-nuggets.com",
|
||||
"ns5.example.com",
|
||||
"ns6.example.com",
|
||||
"ns7.example.com",
|
||||
"ns8.example.com",
|
||||
"ns9.example.com",
|
||||
"ns10.example.com",
|
||||
"ns11.example.com",
|
||||
"ns12.example.com",
|
||||
"ns13.example.com",
|
||||
],
|
||||
)
|
||||
|
||||
infoDomainNoHost = fakedEppObject(
|
||||
"my-nameserver.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
|
@ -1587,6 +1647,26 @@ class MockEppLib(TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
noNameserver = fakedEppObject(
|
||||
"nonameserver.com",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="securityContact",
|
||||
type=PublicContact.ContactTypeChoices.SECURITY,
|
||||
),
|
||||
common.DomainContact(
|
||||
contact="technicalContact",
|
||||
type=PublicContact.ContactTypeChoices.TECHNICAL,
|
||||
),
|
||||
common.DomainContact(
|
||||
contact="adminContact",
|
||||
type=PublicContact.ContactTypeChoices.ADMINISTRATIVE,
|
||||
),
|
||||
],
|
||||
hosts=[],
|
||||
)
|
||||
|
||||
infoDomainCheckHostIPCombo = fakedEppObject(
|
||||
"nameserversubdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
|
@ -1801,10 +1881,13 @@ class MockEppLib(TestCase):
|
|||
"freeman.gov": (self.InfoDomainWithContacts, None),
|
||||
"threenameserversdomain.gov": (self.infoDomainThreeHosts, None),
|
||||
"fournameserversdomain.gov": (self.infoDomainFourHosts, None),
|
||||
"twelvenameserversdomain.gov": (self.infoDomainTwelveHosts, None),
|
||||
"thirteennameserversdomain.gov": (self.infoDomainThirteenHosts, None),
|
||||
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
|
||||
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
|
||||
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
|
||||
"justnameserver.com": (self.justNameserver, None),
|
||||
"nonameserver.com": (self.noNameserver, None),
|
||||
"meoward.gov": (self.mockDataInfoDomainSubdomain, None),
|
||||
"meow.gov": (self.mockDataInfoDomainSubdomainAndIPAddress, None),
|
||||
"fakemeow.gov": (self.mockDataInfoDomainNotSubdomainNoIP, None),
|
||||
|
@ -1877,14 +1960,23 @@ class MockEppLib(TestCase):
|
|||
self.mockedSendFunction = self.mockSendPatch.start()
|
||||
self.mockedSendFunction.side_effect = self.mockSend
|
||||
|
||||
def _convertPublicContactToEpp(self, contact: PublicContact, disclose_email=False, createContact=True):
|
||||
def _convertPublicContactToEpp(
|
||||
self,
|
||||
contact: PublicContact,
|
||||
disclose=False,
|
||||
createContact=True,
|
||||
disclose_fields=None,
|
||||
disclose_types=None,
|
||||
):
|
||||
DF = common.DiscloseField
|
||||
fields = {DF.EMAIL}
|
||||
if disclose_fields is None:
|
||||
fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT, DF.EMAIL}
|
||||
disclose_fields = {field for field in DF} - fields
|
||||
|
||||
di = common.Disclose(
|
||||
flag=disclose_email,
|
||||
fields=fields,
|
||||
)
|
||||
if disclose_types is None:
|
||||
disclose_types = {DF.ADDR: "loc"}
|
||||
|
||||
di = common.Disclose(flag=disclose, fields=disclose_fields, types=disclose_types)
|
||||
|
||||
# check docs here looks like we may have more than one address field but
|
||||
addr = common.ContactAddr(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,14 +17,17 @@ from registrar.models import (
|
|||
Host,
|
||||
Portfolio,
|
||||
)
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from .common import (
|
||||
MockSESClient,
|
||||
completed_domain_request,
|
||||
less_console_noise,
|
||||
create_superuser,
|
||||
create_user,
|
||||
create_omb_analyst_user,
|
||||
create_ready_domain,
|
||||
MockEppLib,
|
||||
GenericTestHelper,
|
||||
|
@ -48,7 +51,9 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
@classmethod
|
||||
def setUpClass(self):
|
||||
super().setUpClass()
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.site = AdminSite()
|
||||
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
|
@ -56,6 +61,24 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
def setUp(self):
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.staffuser)
|
||||
self.nonfebdomain = Domain.objects.create(name="nonfebexample.com")
|
||||
self.febdomain = Domain.objects.create(name="febexample.com", state=Domain.State.READY)
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
self.portfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.staffuser,
|
||||
)
|
||||
self.domain_info = DomainInformation.objects.create(
|
||||
domain=self.febdomain, portfolio=self.portfolio, creator=self.staffuser
|
||||
)
|
||||
self.nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.staffuser,
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -65,12 +88,134 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
User.objects.all().delete()
|
||||
super().tearDownClass()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view domain list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domain_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
self.assertNotContains(response, self.nonfebdomain.name)
|
||||
self.assertNotContains(response, ">Import<")
|
||||
self.assertNotContains(response, ">Export<")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can view/edit federal executive branch domains."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.nonfebdomain.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.febdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
# test portfolio dropdown
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
self.assertNotContains(response, self.nonfebportfolio.organization_name)
|
||||
# test buttons
|
||||
self.assertNotContains(response, "Manage domain")
|
||||
self.assertNotContains(response, "Get registry status")
|
||||
self.assertNotContains(response, "Extend expiration date")
|
||||
self.assertNotContains(response, "Remove from registry")
|
||||
self.assertContains(response, "Place hold")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertNotContains(response, ">Delete<")
|
||||
# test whether fields are readonly or editable
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertNotContains(response, "id_domain_info-0-sub_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-creator")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-anything_else")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_first_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_last_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_email")
|
||||
self.assertNotContains(response, "id_domain_info-0-domain_request")
|
||||
self.assertNotContains(response, "id_domain_info-0-notes")
|
||||
self.assertNotContains(response, "id_domain_info-0-senior_official")
|
||||
self.assertNotContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-state_territory")
|
||||
self.assertNotContains(response, "id_domain_info-0-address_line1")
|
||||
self.assertNotContains(response, "id_domain_info-0-address_line2")
|
||||
self.assertNotContains(response, "id_domain_info-0-city")
|
||||
self.assertNotContains(response, "id_domain_info-0-zipcode")
|
||||
self.assertNotContains(response, "id_domain_info-0-urbanization")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_federal_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_organization_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_state_territory")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_address_line1")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_address_line2")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_city")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_zipcode")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_urbanization")
|
||||
self.assertNotContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-tribe_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-federally_recognized_tribe")
|
||||
self.assertNotContains(response, "id_domain_info-0-state_recognized_tribe")
|
||||
self.assertNotContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertNotContains(response, "id_domain_info-0-sub_organization")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_change(self):
|
||||
"""Ensure super user can view/edit all domains."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.nonfebdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.febdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
# test portfolio dropdown
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
# test buttons
|
||||
self.assertContains(response, "Manage domain")
|
||||
self.assertContains(response, "Get registry status")
|
||||
self.assertContains(response, "Extend expiration date")
|
||||
self.assertContains(response, "Remove from registry")
|
||||
self.assertContains(response, "Place hold")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, ">Delete<")
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertContains(response, "id_domain_info-0-sub_organization")
|
||||
self.assertContains(response, "id_domain_info-0-creator")
|
||||
self.assertContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertContains(response, "id_domain_info-0-anything_else")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_first_name")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_last_name")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_email")
|
||||
self.assertContains(response, "id_domain_info-0-domain_request")
|
||||
self.assertContains(response, "id_domain_info-0-notes")
|
||||
self.assertContains(response, "id_domain_info-0-senior_official")
|
||||
self.assertContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertContains(response, "id_domain_info-0-state_territory")
|
||||
self.assertContains(response, "id_domain_info-0-address_line1")
|
||||
self.assertContains(response, "id_domain_info-0-address_line2")
|
||||
self.assertContains(response, "id_domain_info-0-city")
|
||||
self.assertContains(response, "id_domain_info-0-zipcode")
|
||||
self.assertContains(response, "id_domain_info-0-urbanization")
|
||||
self.assertContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertContains(response, "id_domain_info-0-federal_type")
|
||||
self.assertContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertContains(response, "id_domain_info-0-tribe_name")
|
||||
self.assertContains(response, "id_domain_info-0-federally_recognized_tribe")
|
||||
self.assertContains(response, "id_domain_info-0-state_recognized_tribe")
|
||||
self.assertContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertContains(response, "id_domain_info-0-sub_organization")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_staff_can_see_cisa_region_federal(self):
|
||||
"""Tests if staff can see CISA Region: N/A"""
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import datetime
|
||||
from django.forms import ValidationError
|
||||
from django.utils import timezone
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from waffle.testutils import override_flag
|
||||
import re
|
||||
from django.test import RequestFactory, Client, TestCase, override_settings
|
||||
|
@ -37,6 +39,7 @@ from .common import (
|
|||
less_console_noise,
|
||||
create_superuser,
|
||||
create_user,
|
||||
create_omb_analyst_user,
|
||||
multiple_unalphabetical_domain_objects,
|
||||
MockEppLib,
|
||||
GenericTestHelper,
|
||||
|
@ -68,6 +71,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site)
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.ombanalyst = create_omb_analyst_user()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.test_helper = GenericTestHelper(
|
||||
factory=self.factory,
|
||||
|
@ -80,6 +84,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
allowed_emails = [AllowedEmail(email="mayor@igorville.gov"), AllowedEmail(email="help@get.gov")]
|
||||
AllowedEmail.objects.bulk_create(allowed_emails)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Host.objects.all().delete()
|
||||
|
@ -92,6 +102,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
SeniorOfficial.objects.all().delete()
|
||||
Suborganization.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
self.mock_client.EMAILS_SENT.clear()
|
||||
|
||||
@classmethod
|
||||
|
@ -100,6 +111,71 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
User.objects.all().delete()
|
||||
AllowedEmail.objects.all().delete()
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view domain request list."""
|
||||
febportfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebdomainrequest = completed_domain_request(
|
||||
name="test1234nonfeb.gov",
|
||||
portfolio=nonfebportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
febdomainrequest = completed_domain_request(
|
||||
name="test1234feb.gov",
|
||||
portfolio=febportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
self.client.force_login(self.ombanalyst)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, febdomainrequest.requested_domain.name)
|
||||
self.assertNotContains(response, nonfebdomainrequest.requested_domain.name)
|
||||
self.assertNotContains(response, ">Import<")
|
||||
self.assertNotContains(response, ">Export<")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can view/edit federal executive branch domain requests."""
|
||||
self.client.force_login(self.ombanalyst)
|
||||
febportfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebdomainrequest = completed_domain_request(
|
||||
name="test1234nonfeb.gov",
|
||||
portfolio=nonfebportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
febdomainrequest = completed_domain_request(
|
||||
name="test1234feb.gov",
|
||||
portfolio=febportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_change", args=[nonfebdomainrequest.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_change", args=[febdomainrequest.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, febdomainrequest.requested_domain.name)
|
||||
# test buttons
|
||||
self.assertContains(response, "Save")
|
||||
self.assertNotContains(response, ">Delete<")
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_clean_validates_duplicate_suborganization(self):
|
||||
|
@ -1981,7 +2057,14 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"senior_official",
|
||||
"approved_domain",
|
||||
"requested_domain",
|
||||
"feb_naming_requirements",
|
||||
"feb_naming_requirements_details",
|
||||
"feb_purpose_choice",
|
||||
"purpose",
|
||||
"has_timeframe",
|
||||
"time_frame_details",
|
||||
"is_interagency_initiative",
|
||||
"interagency_initiative_details",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"has_anything_else_text",
|
||||
|
@ -2065,6 +2148,86 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_readonly_fields_for_omb_analyst(self):
|
||||
with less_console_noise():
|
||||
request = self.factory.get("/") # Use the correct method and path
|
||||
request.user = self.ombanalyst
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
expected_fields = [
|
||||
"portfolio_senior_official",
|
||||
"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",
|
||||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"is_election_board",
|
||||
"status_history",
|
||||
"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",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_saving_when_restricted_creator(self):
|
||||
with less_console_noise():
|
||||
# Create an instance of the model
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch, MagicMock, ANY
|
||||
from datetime import date
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.portfolio import Portfolio
|
||||
|
@ -13,13 +13,15 @@ from registrar.utility.email_invitations import (
|
|||
_send_portfolio_admin_addition_emails_to_portfolio_admins,
|
||||
_send_portfolio_admin_removal_emails_to_portfolio_admins,
|
||||
send_domain_invitation_email,
|
||||
send_emails_to_domain_managers,
|
||||
_send_domain_invitation_update_emails_to_domain_managers,
|
||||
send_domain_manager_removal_emails_to_domain_managers,
|
||||
send_portfolio_admin_addition_emails,
|
||||
send_portfolio_admin_removal_emails,
|
||||
send_portfolio_invitation_email,
|
||||
send_portfolio_invitation_remove_email,
|
||||
send_portfolio_member_permission_remove_email,
|
||||
send_portfolio_member_permission_update_email,
|
||||
send_portfolio_update_emails_to_portfolio_admins,
|
||||
)
|
||||
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
|
@ -33,7 +35,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._send_domain_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||
def test_send_domain_invitation_email(
|
||||
self,
|
||||
|
@ -98,7 +100,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._send_domain_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||
def test_send_domain_invitation_email_multiple_domains(
|
||||
self,
|
||||
|
@ -234,7 +236,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
@less_console_noise_decorator
|
||||
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._send_domain_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||
def test_send_domain_invitation_email_raises_sending_email_exception(
|
||||
self,
|
||||
|
@ -281,10 +283,10 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
self.assertEqual(str(context.exception), "Error sending email")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.utility.email_invitations.send_emails_to_domain_managers")
|
||||
@patch("registrar.utility.email_invitations._send_domain_invitation_update_emails_to_domain_managers")
|
||||
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._send_domain_invitation_email")
|
||||
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||
def test_send_domain_invitation_email_manager_emails_send_mail_exception(
|
||||
self,
|
||||
|
@ -295,7 +297,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
mock_send_domain_manager_emails,
|
||||
):
|
||||
"""Test sending domain invitation email for one domain and assert exception
|
||||
when send_emails_to_domain_managers fails.
|
||||
when _send_domain_invitation_update_emails_to_domain_managers fails.
|
||||
"""
|
||||
# Setup
|
||||
mock_domain = MagicMock(name="domain1")
|
||||
|
@ -354,7 +356,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
mock_send_templated_email.return_value = None # No exception means success
|
||||
|
||||
# Call function
|
||||
result = send_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
result = _send_domain_invitation_update_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
|
||||
# Assertions
|
||||
self.assertTrue(result) # All emails should be successfully sent
|
||||
|
@ -394,7 +396,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
mock_send_templated_email.side_effect = EmailSendingError("Email sending failed")
|
||||
|
||||
# Call function
|
||||
result = send_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
result = _send_domain_invitation_update_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
|
||||
# Assertions
|
||||
self.assertFalse(result) # The result should be False as email sending failed
|
||||
|
@ -426,7 +428,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
mock_filter.return_value = []
|
||||
|
||||
# Call function
|
||||
result = send_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
result = _send_domain_invitation_update_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
|
||||
# Assertions
|
||||
self.assertTrue(result) # No emails to send, so it should return True
|
||||
|
@ -457,7 +459,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
|||
mock_send_templated_email.side_effect = [None, EmailSendingError("Failed to send email")]
|
||||
|
||||
# Call function
|
||||
result = send_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
result = _send_domain_invitation_update_emails_to_domain_managers(mock_email, mock_requestor_email, mock_domain)
|
||||
|
||||
# Assertions
|
||||
self.assertFalse(result) # One email failed, so result should be False
|
||||
|
@ -1112,3 +1114,208 @@ class TestSendPortfolioInvitationRemoveEmail(unittest.TestCase):
|
|||
|
||||
# Assertions
|
||||
mock_logger.warning.assert_not_called() # Function should fail before logging email failure
|
||||
|
||||
|
||||
class SendDomainManagerRemovalEmailsToManagersTests(unittest.TestCase):
|
||||
"""Unit tests for send_domain_manager_removal_emails_to_domain_managers function."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.email = "removed.admin@example.com"
|
||||
self.requestor_email = "requestor@example.com"
|
||||
self.domain = MagicMock(spec=Domain)
|
||||
self.domain.name = "Test Domain"
|
||||
|
||||
# Mock domain manager users
|
||||
self.manager_user1 = MagicMock(spec=User)
|
||||
self.manager_user1.email = "manager1@example.com"
|
||||
|
||||
self.manager_user2 = MagicMock(spec=User)
|
||||
self.manager_user2.email = "manager2@example.com"
|
||||
|
||||
self.domain_manager1 = MagicMock(spec=UserDomainRole)
|
||||
self.domain_manager1.user = self.manager_user1
|
||||
|
||||
self.domain_manager2 = MagicMock(spec=UserDomainRole)
|
||||
self.domain_manager2.user = self.manager_user2
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||
def test_send_email_success(self, mock_filter, mock_send_templated_email):
|
||||
"""Test successful sending of domain manager removal emails."""
|
||||
mock_filter.return_value.exclude.return_value = [self.domain_manager1]
|
||||
mock_send_templated_email.return_value = None # No exception means success
|
||||
|
||||
result = send_domain_manager_removal_emails_to_domain_managers(
|
||||
removed_by_user=self.manager_user1,
|
||||
manager_removed=self.manager_user2,
|
||||
manager_removed_email=self.manager_user2.email,
|
||||
domain=self.domain,
|
||||
)
|
||||
|
||||
mock_filter.assert_called_once_with(domain=self.domain)
|
||||
mock_send_templated_email.assert_any_call(
|
||||
"emails/domain_manager_deleted_notification.txt",
|
||||
"emails/domain_manager_deleted_notification_subject.txt",
|
||||
to_address=self.manager_user1.email,
|
||||
context={
|
||||
"domain": self.domain,
|
||||
"removed_by": self.manager_user1,
|
||||
"manager_removed_email": self.manager_user2.email,
|
||||
"date": date.today(),
|
||||
},
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||
def test_send_email_success_when_no_user(self, mock_filter, mock_send_templated_email):
|
||||
"""Test successful sending of domain manager removal emails."""
|
||||
mock_filter.return_value = [self.domain_manager1, self.domain_manager2]
|
||||
mock_send_templated_email.return_value = None # No exception means success
|
||||
|
||||
result = send_domain_manager_removal_emails_to_domain_managers(
|
||||
removed_by_user=self.manager_user1,
|
||||
manager_removed=None,
|
||||
manager_removed_email=self.manager_user2.email,
|
||||
domain=self.domain,
|
||||
)
|
||||
|
||||
mock_filter.assert_called_once_with(domain=self.domain)
|
||||
mock_send_templated_email.assert_any_call(
|
||||
"emails/domain_manager_deleted_notification.txt",
|
||||
"emails/domain_manager_deleted_notification_subject.txt",
|
||||
to_address=self.manager_user1.email,
|
||||
context={
|
||||
"domain": self.domain,
|
||||
"removed_by": self.manager_user1,
|
||||
"manager_removed_email": self.manager_user2.email,
|
||||
"date": date.today(),
|
||||
},
|
||||
)
|
||||
mock_send_templated_email.assert_any_call(
|
||||
"emails/domain_manager_deleted_notification.txt",
|
||||
"emails/domain_manager_deleted_notification_subject.txt",
|
||||
to_address=self.manager_user2.email,
|
||||
context={
|
||||
"domain": self.domain,
|
||||
"removed_by": self.manager_user1,
|
||||
"manager_removed_email": self.manager_user2.email,
|
||||
"date": date.today(),
|
||||
},
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.utility.email_invitations.send_templated_email", side_effect=EmailSendingError)
|
||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||
def test_send_email_failure(self, mock_filter, mock_send_templated_email):
|
||||
"""Test handling of failure in sending admin removal emails."""
|
||||
mock_filter.return_value.exclude.return_value = [self.domain_manager1, self.domain_manager2]
|
||||
|
||||
result = send_domain_manager_removal_emails_to_domain_managers(
|
||||
removed_by_user=self.manager_user1,
|
||||
manager_removed=self.manager_user2,
|
||||
manager_removed_email=self.manager_user2.email,
|
||||
domain=self.domain,
|
||||
)
|
||||
|
||||
self.assertFalse(result)
|
||||
mock_filter.assert_called_once_with(domain=self.domain)
|
||||
mock_send_templated_email.assert_any_call(
|
||||
"emails/domain_manager_deleted_notification.txt",
|
||||
"emails/domain_manager_deleted_notification_subject.txt",
|
||||
to_address=self.manager_user1.email,
|
||||
context={
|
||||
"domain": self.domain,
|
||||
"removed_by": self.manager_user1,
|
||||
"manager_removed_email": self.manager_user2.email,
|
||||
"date": date.today(),
|
||||
},
|
||||
)
|
||||
mock_send_templated_email.assert_any_call(
|
||||
"emails/domain_manager_deleted_notification.txt",
|
||||
"emails/domain_manager_deleted_notification_subject.txt",
|
||||
to_address=self.manager_user2.email,
|
||||
context={
|
||||
"domain": self.domain,
|
||||
"removed_by": self.manager_user1,
|
||||
"manager_removed_email": self.manager_user2.email,
|
||||
"date": date.today(),
|
||||
},
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||
def test_no_managers_to_notify(self, mock_filter):
|
||||
"""Test case where there are no domain managers to notify."""
|
||||
mock_filter.return_value.exclude.return_value = [] # No managers
|
||||
|
||||
result = send_domain_manager_removal_emails_to_domain_managers(
|
||||
removed_by_user=self.manager_user1,
|
||||
manager_removed=self.manager_user2,
|
||||
manager_removed_email=self.manager_user2.email,
|
||||
domain=self.domain,
|
||||
)
|
||||
|
||||
self.assertTrue(result) # No emails sent, but also no failures
|
||||
mock_filter.assert_called_once_with(domain=self.domain)
|
||||
|
||||
|
||||
class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase):
|
||||
"""Unit tests for send_portfolio_update_emails_to_portfolio_admins function."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.email = "removed.admin@example.com"
|
||||
self.requestor_email = "requestor@example.com"
|
||||
self.portfolio = MagicMock(spec=Portfolio, name="Portfolio")
|
||||
self.portfolio.organization_name = "Test Organization"
|
||||
|
||||
# Mock portfolio admin users
|
||||
self.admin_user1 = MagicMock(spec=User)
|
||||
self.admin_user1.email = "admin1@example.com"
|
||||
|
||||
self.admin_user2 = MagicMock(spec=User)
|
||||
self.admin_user2.email = "admin2@example.com"
|
||||
|
||||
self.portfolio_admin1 = MagicMock(spec=UserPortfolioPermission)
|
||||
self.portfolio_admin1.user = self.admin_user1
|
||||
self.portfolio_admin1.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
|
||||
self.portfolio_admin2 = MagicMock(spec=UserPortfolioPermission)
|
||||
self.portfolio_admin2.user = self.admin_user2
|
||||
self.portfolio_admin2.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
|
||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||
@patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
|
||||
def test_send_portfolio_update_emails_to_portfolio_admins(self, mock_filter, mock_send_templated_email):
|
||||
"""Test send_portfolio_update_emails_to_portfolio_admins sends templated email."""
|
||||
# Mock data
|
||||
editor = self.admin_user1
|
||||
updated_page = "Organization"
|
||||
|
||||
mock_filter.return_value = [self.portfolio_admin1, self.portfolio_admin2]
|
||||
mock_send_templated_email.return_value = None # No exception means success
|
||||
|
||||
# Call function
|
||||
result = send_portfolio_update_emails_to_portfolio_admins(editor, self.portfolio, updated_page)
|
||||
|
||||
mock_filter.assert_called_once_with(
|
||||
portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
mock_send_templated_email.assert_any_call(
|
||||
"emails/portfolio_org_update_notification.txt",
|
||||
"emails/portfolio_org_update_notification_subject.txt",
|
||||
to_address=self.admin_user1.email,
|
||||
context=ANY,
|
||||
)
|
||||
mock_send_templated_email.assert_any_call(
|
||||
"emails/portfolio_org_update_notification.txt",
|
||||
"emails/portfolio_org_update_notification_subject.txt",
|
||||
to_address=self.admin_user2.email,
|
||||
context=ANY,
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
|
|
@ -14,10 +14,11 @@ from registrar.forms.domain_request_wizard import (
|
|||
OtherContactsForm,
|
||||
RequirementsForm,
|
||||
TribalGovernmentForm,
|
||||
PurposeForm,
|
||||
AnythingElseForm,
|
||||
AboutYourOrganizationForm,
|
||||
)
|
||||
from registrar.forms import PurposeDetailsForm
|
||||
|
||||
from registrar.forms.domain import ContactForm
|
||||
from registrar.forms.portfolio import (
|
||||
PortfolioInvitedMemberForm,
|
||||
|
@ -257,7 +258,7 @@ class TestFormValidation(MockEppLib):
|
|||
@less_console_noise_decorator
|
||||
def test_purpose_form_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = PurposeForm(
|
||||
form = PurposeDetailsForm(
|
||||
data={
|
||||
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue