mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-20 08:24:05 +02:00
Merge branch 'za/3579-update-public-contact-defaults' into hotgov/3585-update-existing-public-contacts
This commit is contained in:
commit
69d2b4d3b2
37 changed files with 2472 additions and 636 deletions
|
@ -316,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.
|
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.
|
1. Version numbers can be manually controlled in `package.json`. Edit that, if desired.
|
||||||
2. Now run `docker-compose run node npm update`.
|
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. Then run `docker-compose up` to recompile and recopy the assets, or run `docker-compose updateUswds` if your docker is already up.
|
3. Make note of the dotgov changes in uswds-edited.js (Ctrl-F DOTGOV for modifications to USWDS compiled code).
|
||||||
4. Make note of the dotgov changes in uswds-edited.js.
|
4. Copy over the newly compiled code from uswds.js into uswds-edited.js.
|
||||||
5. 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. 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. 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
|
## Finite State Machines
|
||||||
|
|
||||||
|
|
292
src/package-lock.json
generated
292
src/package-lock.json
generated
|
@ -63,23 +63,22 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.26.8",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
|
||||||
"integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==",
|
"integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@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-compilation-targets": "^7.26.5",
|
||||||
"@babel/helper-module-transforms": "^7.26.0",
|
"@babel/helper-module-transforms": "^7.26.0",
|
||||||
"@babel/helpers": "^7.26.7",
|
"@babel/helpers": "^7.26.9",
|
||||||
"@babel/parser": "^7.26.8",
|
"@babel/parser": "^7.26.9",
|
||||||
"@babel/template": "^7.26.8",
|
"@babel/template": "^7.26.9",
|
||||||
"@babel/traverse": "^7.26.8",
|
"@babel/traverse": "^7.26.9",
|
||||||
"@babel/types": "^7.26.8",
|
"@babel/types": "^7.26.9",
|
||||||
"@types/gensync": "^1.0.0",
|
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"gensync": "^1.0.0-beta.2",
|
"gensync": "^1.0.0-beta.2",
|
||||||
|
@ -95,14 +94,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.26.8",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
|
||||||
"integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==",
|
"integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.26.8",
|
"@babel/parser": "^7.26.9",
|
||||||
"@babel/types": "^7.26.8",
|
"@babel/types": "^7.26.9",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
|
@ -142,18 +141,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||||
"version": "7.25.9",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz",
|
||||||
"integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==",
|
"integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.25.9",
|
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||||
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||||
"@babel/helper-optimise-call-expression": "^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/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
||||||
"@babel/traverse": "^7.25.9",
|
"@babel/traverse": "^7.26.9",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -363,27 +362,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers": {
|
"node_modules/@babel/helpers": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
|
||||||
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
|
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.25.9",
|
"@babel/template": "^7.26.9",
|
||||||
"@babel/types": "^7.26.7"
|
"@babel/types": "^7.26.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.26.8",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
|
||||||
"integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
|
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.8"
|
"@babel/types": "^7.26.9"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
|
@ -809,13 +808,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-for-of": {
|
"node_modules/@babel/plugin-transform-for-of": {
|
||||||
"version": "7.25.9",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz",
|
||||||
"integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
|
"integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.25.9",
|
"@babel/helper-plugin-utils": "^7.26.5",
|
||||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -1376,9 +1375,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env": {
|
"node_modules/@babel/preset-env": {
|
||||||
"version": "7.26.8",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz",
|
||||||
"integrity": "sha512-um7Sy+2THd697S4zJEfv/U5MHGJzkN2xhtsR3T/SWRbVSic62nbISh51VVfU9JiO/L/Z97QczHTaFVkOU8IzNg==",
|
"integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1411,7 +1410,7 @@
|
||||||
"@babel/plugin-transform-dynamic-import": "^7.25.9",
|
"@babel/plugin-transform-dynamic-import": "^7.25.9",
|
||||||
"@babel/plugin-transform-exponentiation-operator": "^7.26.3",
|
"@babel/plugin-transform-exponentiation-operator": "^7.26.3",
|
||||||
"@babel/plugin-transform-export-namespace-from": "^7.25.9",
|
"@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-function-name": "^7.25.9",
|
||||||
"@babel/plugin-transform-json-strings": "^7.25.9",
|
"@babel/plugin-transform-json-strings": "^7.25.9",
|
||||||
"@babel/plugin-transform-literals": "^7.25.9",
|
"@babel/plugin-transform-literals": "^7.25.9",
|
||||||
|
@ -1475,9 +1474,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
|
||||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1488,32 +1487,32 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.26.8",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
||||||
"integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==",
|
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/parser": "^7.26.8",
|
"@babel/parser": "^7.26.9",
|
||||||
"@babel/types": "^7.26.8"
|
"@babel/types": "^7.26.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.26.8",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
|
||||||
"integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==",
|
"integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/generator": "^7.26.8",
|
"@babel/generator": "^7.26.9",
|
||||||
"@babel/parser": "^7.26.8",
|
"@babel/parser": "^7.26.9",
|
||||||
"@babel/template": "^7.26.8",
|
"@babel/template": "^7.26.9",
|
||||||
"@babel/types": "^7.26.8",
|
"@babel/types": "^7.26.9",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
|
@ -1522,9 +1521,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.26.8",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
|
||||||
"integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
|
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1999,13 +1998,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
@ -2014,9 +2006,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.13.1",
|
"version": "22.13.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||||
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
|
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -2255,9 +2247,9 @@
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.0",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -2868,9 +2860,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001699",
|
"version": "1.0.30001702",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz",
|
||||||
"integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==",
|
"integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -3142,13 +3134,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.40.0",
|
"version": "3.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
||||||
"integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==",
|
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.3"
|
"browserslist": "^4.24.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
@ -3371,9 +3363,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.97",
|
"version": "1.5.113",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.113.tgz",
|
||||||
"integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==",
|
"integrity": "sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
@ -3661,13 +3653,6 @@
|
||||||
"node": ">=8.6.0"
|
"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": {
|
"node_modules/fast-levenshtein": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz",
|
||||||
|
@ -3706,9 +3691,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.0",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
|
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -5718,16 +5703,6 @@
|
||||||
"once": "^1.3.1"
|
"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": {
|
"node_modules/puppeteer": {
|
||||||
"version": "9.1.1",
|
"version": "9.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
|
||||||
|
@ -6113,9 +6088,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -6164,9 +6139,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.8.1",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -6186,9 +6161,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.84.0",
|
"version": "1.85.1",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
|
||||||
"integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==",
|
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
|
@ -6661,9 +6636,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass/node_modules/readdirp": {
|
"node_modules/sass/node_modules/readdirp": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
"integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==",
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">= 14.18.0"
|
||||||
|
@ -6978,9 +6953,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.38.2",
|
"version": "5.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.38.2.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
||||||
"integrity": "sha512-w8CXxxbFA5zfNsR/i8HZq5bvn18AK0O9jj7hyo1YqkovLxEFa0uP0LCVGZRqiRaKRFxXhELBp8SteeAjEnfeJg==",
|
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -6997,9 +6972,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser-webpack-plugin": {
|
"node_modules/terser-webpack-plugin": {
|
||||||
"version": "5.3.11",
|
"version": "5.3.14",
|
||||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||||
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
|
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -7229,9 +7204,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
|
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -7259,16 +7234,6 @@
|
||||||
"browserslist": ">= 4.21.0"
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -7522,9 +7487,9 @@
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.97.1",
|
"version": "5.98.0",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
|
||||||
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
|
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -7546,9 +7511,9 @@
|
||||||
"loader-runner": "^4.2.0",
|
"loader-runner": "^4.2.0",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"neo-async": "^2.6.2",
|
"neo-async": "^2.6.2",
|
||||||
"schema-utils": "^3.2.0",
|
"schema-utils": "^4.3.0",
|
||||||
"tapable": "^2.1.1",
|
"tapable": "^2.1.1",
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"watchpack": "^2.4.1",
|
"watchpack": "^2.4.1",
|
||||||
"webpack-sources": "^3.2.3"
|
"webpack-sources": "^3.2.3"
|
||||||
},
|
},
|
||||||
|
@ -7617,59 +7582,6 @@
|
||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"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": {
|
"node_modules/whatwg-encoding": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
@ -7842,9 +7754,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.0.tgz",
|
||||||
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
|
"integrity": "sha512-KHBC7z61OJeaMGnF3wqNZj+GGNXOyypZviiKpQeiHirG5Ib1ImwcLBH70rbMSkKfSmUNBsdf2PwaEJtKvgmkNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -75,6 +75,19 @@ from django.utils.translation import gettext_lazy as _
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportExportRegistrarModelAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
def has_import_permission(self, request):
|
||||||
|
return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm(
|
||||||
|
"registrar.full_access_permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_export_permission(self, request):
|
||||||
|
return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm(
|
||||||
|
"registrar.full_access_permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FsmModelResource(resources.ModelResource):
|
class FsmModelResource(resources.ModelResource):
|
||||||
"""ModelResource is extended to support importing of tables which
|
"""ModelResource is extended to support importing of tables which
|
||||||
have FSMFields. ModelResource is extended with the following changes
|
have FSMFields. ModelResource is extended with the following changes
|
||||||
|
@ -465,7 +478,7 @@ class DomainRequestAdminForm(forms.ModelForm):
|
||||||
# only set the available transitions if the user is not restricted
|
# only set the available transitions if the user is not restricted
|
||||||
# from editing the domain request; otherwise, the form will be
|
# from editing the domain request; otherwise, the form will be
|
||||||
# readonly and the status field will not have a widget
|
# readonly and the status field will not have a widget
|
||||||
if not domain_request.creator.is_restricted():
|
if not domain_request.creator.is_restricted() and "status" in self.fields:
|
||||||
self.fields["status"].widget.choices = available_transitions
|
self.fields["status"].widget.choices = available_transitions
|
||||||
|
|
||||||
def get_custom_field_transitions(self, instance, field):
|
def get_custom_field_transitions(self, instance, field):
|
||||||
|
@ -919,7 +932,7 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
class MyUserAdmin(BaseUserAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
resource_classes = [UserResource]
|
resource_classes = [UserResource]
|
||||||
|
@ -1224,7 +1237,7 @@ class HostResource(resources.ModelResource):
|
||||||
model = models.Host
|
model = models.Host
|
||||||
|
|
||||||
|
|
||||||
class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
|
class MyHostAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom host admin class to use our inlines."""
|
"""Custom host admin class to use our inlines."""
|
||||||
|
|
||||||
resource_classes = [HostResource]
|
resource_classes = [HostResource]
|
||||||
|
@ -1242,7 +1255,7 @@ class HostIpResource(resources.ModelResource):
|
||||||
model = models.HostIP
|
model = models.HostIP
|
||||||
|
|
||||||
|
|
||||||
class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
|
class HostIpAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom host ip admin class"""
|
"""Custom host ip admin class"""
|
||||||
|
|
||||||
resource_classes = [HostIpResource]
|
resource_classes = [HostIpResource]
|
||||||
|
@ -1264,7 +1277,7 @@ class ContactResource(resources.ModelResource):
|
||||||
model = models.Contact
|
model = models.Contact
|
||||||
|
|
||||||
|
|
||||||
class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class ContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom contact admin class to add search."""
|
"""Custom contact admin class to add search."""
|
||||||
|
|
||||||
resource_classes = [ContactResource]
|
resource_classes = [ContactResource]
|
||||||
|
@ -1398,6 +1411,59 @@ class SeniorOfficialAdmin(ListHeaderAdmin):
|
||||||
# in autocomplete_fields for Senior Official
|
# in autocomplete_fields for Senior Official
|
||||||
ordering = ["first_name", "last_name"]
|
ordering = ["first_name", "last_name"]
|
||||||
|
|
||||||
|
readonly_fields = []
|
||||||
|
|
||||||
|
# Even though this is empty, I will leave it as a stub for easy changes in the future
|
||||||
|
# rather than strip it out of our logic.
|
||||||
|
analyst_readonly_fields = [] # type: ignore
|
||||||
|
|
||||||
|
omb_analyst_readonly_fields = [
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"title",
|
||||||
|
"phone",
|
||||||
|
"email",
|
||||||
|
"federal_agency",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
"""Set the read-only state on form elements.
|
||||||
|
We have conditions that determine which fields are read-only:
|
||||||
|
admin user permissions and analyst (cisa or omb) status, so
|
||||||
|
we'll use the baseline readonly_fields and extend it as needed.
|
||||||
|
"""
|
||||||
|
readonly_fields = list(self.readonly_fields)
|
||||||
|
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for OMB analysts
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for analysts and
|
||||||
|
# users who might not belong to groups
|
||||||
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Restrict queryset based on user permissions."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE)
|
||||||
|
|
||||||
|
return qs # Return full queryset if the user doesn't have the restriction
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Restrict view permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return obj.federal_agency and obj.federal_agency.federal_type == BranchChoices.EXECUTIVE
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
class WebsiteResource(resources.ModelResource):
|
class WebsiteResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -1407,7 +1473,7 @@ class WebsiteResource(resources.ModelResource):
|
||||||
model = models.Website
|
model = models.Website
|
||||||
|
|
||||||
|
|
||||||
class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class WebsiteAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom website admin class."""
|
"""Custom website admin class."""
|
||||||
|
|
||||||
resource_classes = [WebsiteResource]
|
resource_classes = [WebsiteResource]
|
||||||
|
@ -1508,7 +1574,7 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
||||||
obj.delete() # Calls the overridden delete method on each instance
|
obj.delete() # Calls the overridden delete method on each instance
|
||||||
|
|
||||||
|
|
||||||
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom user domain role admin class."""
|
"""Custom user domain role admin class."""
|
||||||
|
|
||||||
resource_classes = [UserDomainRoleResource]
|
resource_classes = [UserDomainRoleResource]
|
||||||
|
@ -1691,6 +1757,63 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
||||||
# Override for the delete confirmation page on the domain table (bulk delete action)
|
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||||
delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html"
|
delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html"
|
||||||
|
|
||||||
|
def get_annotated_queryset(self, queryset):
|
||||||
|
return queryset.annotate(
|
||||||
|
converted_generic_org_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
domain__domain_info__portfolio__isnull=False,
|
||||||
|
then=F("domain__domain_info__portfolio__organization_type"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("domain__domain_info__generic_org_type"),
|
||||||
|
),
|
||||||
|
converted_federal_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
Q(domain__domain_info__portfolio__isnull=False)
|
||||||
|
& Q(domain__domain_info__portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("domain__domain_info__portfolio__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the federal agency's federal_type
|
||||||
|
default=F("domain__domain_info__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Restrict queryset based on user permissions."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
annotated_qs = self.get_annotated_queryset(qs)
|
||||||
|
return annotated_qs.filter(
|
||||||
|
converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
converted_federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return qs # Return full queryset if the user doesn't have the restriction
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Restrict view permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return (
|
||||||
|
obj.domain.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
and obj.domain.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
|
# Select domain invitations to change -> Domain invitations
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Domain invitations"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
"""Override the change_view to add the invitation obj for the change_form_object_tools template"""
|
"""Override the change_view to add the invitation obj for the change_form_object_tools template"""
|
||||||
|
|
||||||
|
@ -1912,7 +2035,7 @@ class DomainInformationResource(resources.ModelResource):
|
||||||
model = models.DomainInformation
|
model = models.DomainInformation
|
||||||
|
|
||||||
|
|
||||||
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class DomainInformationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Customize domain information admin class."""
|
"""Customize domain information admin class."""
|
||||||
|
|
||||||
class GenericOrgFilter(admin.SimpleListFilter):
|
class GenericOrgFilter(admin.SimpleListFilter):
|
||||||
|
@ -2189,6 +2312,47 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Read only that we'll leverage for OMB Analysts
|
||||||
|
omb_analyst_readonly_fields = [
|
||||||
|
"federal_agency",
|
||||||
|
"creator",
|
||||||
|
"about_your_organization",
|
||||||
|
"anything_else",
|
||||||
|
"cisa_representative_first_name",
|
||||||
|
"cisa_representative_last_name",
|
||||||
|
"cisa_representative_email",
|
||||||
|
"domain_request",
|
||||||
|
"notes",
|
||||||
|
"senior_official",
|
||||||
|
"organization_type",
|
||||||
|
"organization_name",
|
||||||
|
"state_territory",
|
||||||
|
"address_line1",
|
||||||
|
"address_line2",
|
||||||
|
"city",
|
||||||
|
"zipcode",
|
||||||
|
"urbanization",
|
||||||
|
"portfolio_organization_type",
|
||||||
|
"portfolio_federal_type",
|
||||||
|
"portfolio_organization_name",
|
||||||
|
"portfolio_federal_agency",
|
||||||
|
"portfolio_state_territory",
|
||||||
|
"portfolio_address_line1",
|
||||||
|
"portfolio_address_line2",
|
||||||
|
"portfolio_city",
|
||||||
|
"portfolio_zipcode",
|
||||||
|
"portfolio_urbanization",
|
||||||
|
"organization_type",
|
||||||
|
"federal_type",
|
||||||
|
"federal_agency",
|
||||||
|
"tribe_name",
|
||||||
|
"federally_recognized_tribe",
|
||||||
|
"state_recognized_tribe",
|
||||||
|
"about_your_organization",
|
||||||
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
|
]
|
||||||
|
|
||||||
# For each filter_horizontal, init in admin js initFilterHorizontalWidget
|
# For each filter_horizontal, init in admin js initFilterHorizontalWidget
|
||||||
# to activate the edit/delete/view buttons
|
# to activate the edit/delete/view buttons
|
||||||
filter_horizontal = ("other_contacts",)
|
filter_horizontal = ("other_contacts",)
|
||||||
|
@ -2217,6 +2381,10 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
if request.user.has_perm("registrar.full_access_permission"):
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
return readonly_fields
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for OMB analysts
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
# Return restrictive Read-only fields for analysts and
|
# Return restrictive Read-only fields for analysts and
|
||||||
# users who might not belong to groups
|
# users who might not belong to groups
|
||||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
|
@ -2233,6 +2401,38 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
use_sort = db_field.name != "senior_official"
|
use_sort = db_field.name != "senior_official"
|
||||||
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
||||||
|
|
||||||
|
def get_annotated_queryset(self, queryset):
|
||||||
|
return queryset.annotate(
|
||||||
|
conv_generic_org_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("generic_org_type"),
|
||||||
|
),
|
||||||
|
conv_federal_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("portfolio__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
# Otherwise, return the federal_type from federal agency
|
||||||
|
default=F("federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||||
|
request params."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
annotated_qs = self.get_annotated_queryset(qs)
|
||||||
|
return annotated_qs.filter(
|
||||||
|
conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
conv_federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestResource(FsmModelResource):
|
class DomainRequestResource(FsmModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -2242,7 +2442,7 @@ class DomainRequestResource(FsmModelResource):
|
||||||
model = models.DomainRequest
|
model = models.DomainRequest
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom domain requests admin class."""
|
"""Custom domain requests admin class."""
|
||||||
|
|
||||||
resource_classes = [DomainRequestResource]
|
resource_classes = [DomainRequestResource]
|
||||||
|
@ -2300,7 +2500,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
class FederalTypeFilter(admin.SimpleListFilter):
|
class FederalTypeFilter(admin.SimpleListFilter):
|
||||||
"""Custom Federal Type filter that accomodates portfolio feature.
|
"""Custom Federal Type filter that accomodates portfolio feature.
|
||||||
If we have a portfolio, use the portfolio's federal type. If not, use the
|
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||||
organization in the Domain Request object."""
|
organization in the Domain Request object's federal agency."""
|
||||||
|
|
||||||
title = "federal type"
|
title = "federal type"
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
@ -2341,7 +2541,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if self.value():
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__federal_agency__federal_type=self.value())
|
Q(portfolio__federal_agency__federal_type=self.value())
|
||||||
| Q(portfolio__isnull=True, federal_type=self.value())
|
| Q(portfolio__isnull=True, federal_agency__federal_type=self.value())
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -2756,6 +2956,62 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"cisa_representative_email",
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Read only that we'll leverage for OMB Analysts
|
||||||
|
omb_analyst_readonly_fields = [
|
||||||
|
"federal_agency",
|
||||||
|
"creator",
|
||||||
|
"about_your_organization",
|
||||||
|
"requested_domain",
|
||||||
|
"approved_domain",
|
||||||
|
"alternative_domains",
|
||||||
|
"purpose",
|
||||||
|
"no_other_contacts_rationale",
|
||||||
|
"anything_else",
|
||||||
|
"is_policy_acknowledged",
|
||||||
|
"cisa_representative_first_name",
|
||||||
|
"cisa_representative_last_name",
|
||||||
|
"cisa_representative_email",
|
||||||
|
"status",
|
||||||
|
"investigator",
|
||||||
|
"notes",
|
||||||
|
"senior_official",
|
||||||
|
"organization_type",
|
||||||
|
"organization_name",
|
||||||
|
"state_territory",
|
||||||
|
"address_line1",
|
||||||
|
"address_line2",
|
||||||
|
"city",
|
||||||
|
"zipcode",
|
||||||
|
"urbanization",
|
||||||
|
"portfolio_organization_type",
|
||||||
|
"portfolio_federal_type",
|
||||||
|
"portfolio_organization_name",
|
||||||
|
"portfolio_federal_agency",
|
||||||
|
"portfolio_state_territory",
|
||||||
|
"portfolio_address_line1",
|
||||||
|
"portfolio_address_line2",
|
||||||
|
"portfolio_city",
|
||||||
|
"portfolio_zipcode",
|
||||||
|
"portfolio_urbanization",
|
||||||
|
"is_election_board",
|
||||||
|
"organization_type",
|
||||||
|
"federal_type",
|
||||||
|
"federal_agency",
|
||||||
|
"tribe_name",
|
||||||
|
"federally_recognized_tribe",
|
||||||
|
"state_recognized_tribe",
|
||||||
|
"about_your_organization",
|
||||||
|
"rejection_reason",
|
||||||
|
"rejection_reason_email",
|
||||||
|
"action_needed_reason",
|
||||||
|
"action_needed_reason_email",
|
||||||
|
"portfolio",
|
||||||
|
"sub_organization",
|
||||||
|
"requested_suborganization",
|
||||||
|
"suborganization_city",
|
||||||
|
"suborganization_state_territory",
|
||||||
|
]
|
||||||
|
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
"approved_domain",
|
"approved_domain",
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
|
@ -2996,6 +3252,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
if request.user.has_perm("registrar.full_access_permission"):
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
return readonly_fields
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for OMB analysts
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
# Return restrictive Read-only fields for analysts and
|
# Return restrictive Read-only fields for analysts and
|
||||||
# users who might not belong to groups
|
# users who might not belong to groups
|
||||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
|
@ -3179,6 +3439,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
use_sort = db_field.name != "senior_official"
|
use_sort = db_field.name != "senior_official"
|
||||||
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
||||||
|
|
||||||
|
def get_annotated_queryset(self, queryset):
|
||||||
|
return queryset.annotate(
|
||||||
|
conv_generic_org_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||||
|
# Otherwise, return the natively assigned value
|
||||||
|
default=F("generic_org_type"),
|
||||||
|
),
|
||||||
|
conv_federal_type=Case(
|
||||||
|
# When portfolio is present, use its value instead
|
||||||
|
When(
|
||||||
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
|
then=F("portfolio__federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
# Otherwise, return federal type from federal agency
|
||||||
|
default=F("federal_agency__federal_type"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||||
request params."""
|
request params."""
|
||||||
|
@ -3188,8 +3467,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if portfolio_id:
|
if portfolio_id:
|
||||||
# Further filter the queryset by the portfolio
|
# Further filter the queryset by the portfolio
|
||||||
qs = qs.filter(portfolio=portfolio_id)
|
qs = qs.filter(portfolio=portfolio_id)
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
annotated_qs = self.get_annotated_queryset(qs)
|
||||||
|
return annotated_qs.filter(
|
||||||
|
conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
conv_federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Restrict view permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return (
|
||||||
|
obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
and obj.converted_federal_type == BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
"""Restrict update permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return (
|
||||||
|
obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
and obj.converted_federal_type == BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
def get_search_results(self, request, queryset, search_term):
|
def get_search_results(self, request, queryset, search_term):
|
||||||
# Call the parent's method to apply default search logic
|
# Call the parent's method to apply default search logic
|
||||||
base_queryset, use_distinct = super().get_search_results(request, queryset, search_term)
|
base_queryset, use_distinct = super().get_search_results(request, queryset, search_term)
|
||||||
|
@ -3209,6 +3519,15 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
return combined_queryset, use_distinct
|
return combined_queryset, use_distinct
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
# Store attribute in the form for template access
|
||||||
|
form.show_contact_as_plain_text = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class TransitionDomainAdmin(ListHeaderAdmin):
|
class TransitionDomainAdmin(ListHeaderAdmin):
|
||||||
"""Custom transition domain admin class."""
|
"""Custom transition domain admin class."""
|
||||||
|
@ -3240,6 +3559,16 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
template = "django/admin/includes/domain_info_inline_stacked.html"
|
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||||
model = models.DomainInformation
|
model = models.DomainInformation
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the admin class and define a default value for is_omb_analyst."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.is_omb_analyst = False # Default value in case it's accessed before being set
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Ensure self.is_omb_analyst is set early."""
|
||||||
|
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||||
|
return super().get_queryset(request)
|
||||||
|
|
||||||
# Define methods to display fields from the related portfolio
|
# Define methods to display fields from the related portfolio
|
||||||
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
|
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
|
||||||
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
|
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
|
||||||
|
@ -3307,6 +3636,7 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
|
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
|
||||||
readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields)
|
readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields)
|
||||||
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
|
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
|
||||||
|
omb_analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.omb_analyst_readonly_fields)
|
||||||
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
|
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
|
||||||
|
|
||||||
def get_domain_managers(self, obj):
|
def get_domain_managers(self, obj):
|
||||||
|
@ -3327,11 +3657,15 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
if not domain_managers:
|
if not domain_managers:
|
||||||
return "No domain managers found."
|
return "No domain managers found."
|
||||||
|
|
||||||
domain_manager_details = "<table><thead><tr><th>UID</th><th>Name</th><th>Email</th></tr></thead><tbody>"
|
domain_manager_details = "<table><thead><tr>"
|
||||||
|
if not self.is_omb_analyst:
|
||||||
|
domain_manager_details += "<th>UID</th>"
|
||||||
|
domain_manager_details += "<th>Name</th><th>Email</th></tr></thead><tbody>"
|
||||||
for domain_manager in domain_managers:
|
for domain_manager in domain_managers:
|
||||||
full_name = domain_manager.get_formatted_name()
|
full_name = domain_manager.get_formatted_name()
|
||||||
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
|
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
|
||||||
domain_manager_details += "<tr>"
|
domain_manager_details += "<tr>"
|
||||||
|
if not self.is_omb_analyst:
|
||||||
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
|
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
|
||||||
domain_manager_details += f"<td>{escape(full_name)}</td>"
|
domain_manager_details += f"<td>{escape(full_name)}</td>"
|
||||||
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
|
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
|
||||||
|
@ -3364,7 +3698,8 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
|
|
||||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||||
if analyst_perm and not superuser_perm:
|
omb_analyst_perm = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||||
|
if (analyst_perm or omb_analyst_perm) and not superuser_perm:
|
||||||
return True
|
return True
|
||||||
return super().has_change_permission(request, obj)
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
@ -3438,6 +3773,23 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
|
|
||||||
return modified_fieldsets
|
return modified_fieldsets
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
# Store attribute in the form for template access
|
||||||
|
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||||
|
form.show_contact_as_plain_text = self.is_omb_analyst
|
||||||
|
form.is_omb_analyst = self.is_omb_analyst
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
|
"""Attach request to the formset so that it can be available in the form"""
|
||||||
|
formset = super().get_formset(request, obj, **kwargs)
|
||||||
|
formset.form.request = request # Attach request to form
|
||||||
|
return formset
|
||||||
|
|
||||||
|
|
||||||
class DomainResource(FsmModelResource):
|
class DomainResource(FsmModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -3447,7 +3799,7 @@ class DomainResource(FsmModelResource):
|
||||||
model = models.Domain
|
model = models.Domain
|
||||||
|
|
||||||
|
|
||||||
class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class DomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom domain admin class to add extra buttons."""
|
"""Custom domain admin class to add extra buttons."""
|
||||||
|
|
||||||
resource_classes = [DomainResource]
|
resource_classes = [DomainResource]
|
||||||
|
@ -3559,7 +3911,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if self.value():
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(domain_info__portfolio__federal_type=self.value())
|
Q(domain_info__portfolio__federal_type=self.value())
|
||||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
|
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -3586,7 +3938,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
|
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
|
||||||
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
||||||
),
|
),
|
||||||
# Otherwise, return the natively assigned value
|
# Otherwise, return federal type from federal agency
|
||||||
default=F("domain_info__federal_agency__federal_type"),
|
default=F("domain_info__federal_agency__federal_type"),
|
||||||
),
|
),
|
||||||
converted_organization_name=Case(
|
converted_organization_name=Case(
|
||||||
|
@ -4013,8 +4365,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# Fixes a bug wherein users which are only is_staff
|
# Fixes a bug wherein users which are only is_staff
|
||||||
# can access 'change' when GET,
|
# can access 'change' when GET,
|
||||||
# but cannot access this page when it is a request of type POST.
|
# but cannot access this page when it is a request of type POST.
|
||||||
if request.user.has_perm("registrar.full_access_permission") or request.user.has_perm(
|
if (
|
||||||
"registrar.analyst_access_permission"
|
request.user.has_perm("registrar.full_access_permission")
|
||||||
|
or request.user.has_perm("registrar.analyst_access_permission")
|
||||||
|
or request.user.groups.filter(name="omb_analysts_group").exists()
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
return super().has_change_permission(request, obj)
|
return super().has_change_permission(request, obj)
|
||||||
|
@ -4029,8 +4383,37 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if portfolio_id:
|
if portfolio_id:
|
||||||
# Further filter the queryset by the portfolio
|
# Further filter the queryset by the portfolio
|
||||||
qs = qs.filter(domain_info__portfolio=portfolio_id)
|
qs = qs.filter(domain_info__portfolio=portfolio_id)
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return qs.filter(
|
||||||
|
converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
converted_federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Restrict view permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return (
|
||||||
|
obj.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
and obj.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
# Store attribute in the form for template access
|
||||||
|
is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||||
|
form.show_contact_as_plain_text = is_omb_analyst
|
||||||
|
form.is_omb_analyst = is_omb_analyst
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class DraftDomainResource(resources.ModelResource):
|
class DraftDomainResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -4040,7 +4423,7 @@ class DraftDomainResource(resources.ModelResource):
|
||||||
model = models.DraftDomain
|
model = models.DraftDomain
|
||||||
|
|
||||||
|
|
||||||
class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class DraftDomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom draft domain admin class."""
|
"""Custom draft domain admin class."""
|
||||||
|
|
||||||
resource_classes = [DraftDomainResource]
|
resource_classes = [DraftDomainResource]
|
||||||
|
@ -4152,7 +4535,7 @@ class PublicContactResource(resources.ModelResource):
|
||||||
self.after_save_instance(instance, using_transactions, dry_run)
|
self.after_save_instance(instance, using_transactions, dry_run)
|
||||||
|
|
||||||
|
|
||||||
class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class PublicContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
"""Custom PublicContact admin class."""
|
"""Custom PublicContact admin class."""
|
||||||
|
|
||||||
resource_classes = [PublicContactResource]
|
resource_classes = [PublicContactResource]
|
||||||
|
@ -4211,6 +4594,11 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
_meta = Meta()
|
_meta = Meta()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize the admin class and define a default value for is_omb_analyst."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.is_omb_analyst = False # Default value in case it's accessed before being set
|
||||||
|
|
||||||
change_form_template = "django/admin/portfolio_change_form.html"
|
change_form_template = "django/admin/portfolio_change_form.html"
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
# created_on is the created_at field
|
# created_on is the created_at field
|
||||||
|
@ -4292,6 +4680,19 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
# rather than strip it out of our logic.
|
# rather than strip it out of our logic.
|
||||||
analyst_readonly_fields = [] # type: ignore
|
analyst_readonly_fields = [] # type: ignore
|
||||||
|
|
||||||
|
omb_analyst_readonly_fields = [
|
||||||
|
"notes",
|
||||||
|
"organization_type",
|
||||||
|
"organization_name",
|
||||||
|
"federal_agency",
|
||||||
|
"state_territory",
|
||||||
|
"address_line1",
|
||||||
|
"address_line2",
|
||||||
|
"city",
|
||||||
|
"zipcode",
|
||||||
|
"urbanization",
|
||||||
|
]
|
||||||
|
|
||||||
def get_admin_users(self, obj):
|
def get_admin_users(self, obj):
|
||||||
# Filter UserPortfolioPermission objects related to the portfolio
|
# Filter UserPortfolioPermission objects related to the portfolio
|
||||||
admin_permissions = self.get_user_portfolio_permission_admins(obj)
|
admin_permissions = self.get_user_portfolio_permission_admins(obj)
|
||||||
|
@ -4377,6 +4778,8 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
"""Returns the number of administrators for this portfolio"""
|
"""Returns the number of administrators for this portfolio"""
|
||||||
admin_count = len(self.get_user_portfolio_permission_admins(obj))
|
admin_count = len(self.get_user_portfolio_permission_admins(obj))
|
||||||
if admin_count > 0:
|
if admin_count > 0:
|
||||||
|
if self.is_omb_analyst:
|
||||||
|
return format_html(f"{admin_count} administrators")
|
||||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||||
# Create a clickable link with the count
|
# Create a clickable link with the count
|
||||||
return format_html(f'<a href="{url}">{admin_count} admins</a>')
|
return format_html(f'<a href="{url}">{admin_count} admins</a>')
|
||||||
|
@ -4388,6 +4791,8 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
"""Returns the number of basic members for this portfolio"""
|
"""Returns the number of basic members for this portfolio"""
|
||||||
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
|
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
|
||||||
if member_count > 0:
|
if member_count > 0:
|
||||||
|
if self.is_omb_analyst:
|
||||||
|
return format_html(f"{member_count} members")
|
||||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||||
# Create a clickable link with the count
|
# Create a clickable link with the count
|
||||||
return format_html(f'<a href="{url}">{member_count} basic members</a>')
|
return format_html(f'<a href="{url}">{member_count} basic members</a>')
|
||||||
|
@ -4433,12 +4838,35 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
if request.user.has_perm("registrar.full_access_permission"):
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
return readonly_fields
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for OMB analysts
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
# Return restrictive Read-only fields for analysts and
|
# Return restrictive Read-only fields for analysts and
|
||||||
# users who might not belong to groups
|
# users who might not belong to groups
|
||||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
return readonly_fields
|
return readonly_fields
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Restrict queryset based on user permissions."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
self.is_omb_analyst = True
|
||||||
|
return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE)
|
||||||
|
|
||||||
|
return qs # Return full queryset if the user doesn't have the restriction
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Restrict view permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return obj.federal_type == BranchChoices.EXECUTIVE
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
"""Add related suborganizations and domain groups.
|
"""Add related suborganizations and domain groups.
|
||||||
Add the summary for the portfolio members field (list of members that link to change_forms)."""
|
Add the summary for the portfolio members field (list of members that link to change_forms)."""
|
||||||
|
@ -4483,6 +4911,17 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
# Store attribute in the form for template access
|
||||||
|
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||||
|
form.show_contact_as_plain_text = self.is_omb_analyst
|
||||||
|
form.is_omb_analyst = self.is_omb_analyst
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class FederalAgencyResource(resources.ModelResource):
|
class FederalAgencyResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -4492,13 +4931,66 @@ class FederalAgencyResource(resources.ModelResource):
|
||||||
model = models.FederalAgency
|
model = models.FederalAgency
|
||||||
|
|
||||||
|
|
||||||
class FederalAgencyAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
list_display = ["agency"]
|
list_display = ["agency"]
|
||||||
search_fields = ["agency"]
|
search_fields = ["agency"]
|
||||||
search_help_text = "Search by federal agency."
|
search_help_text = "Search by federal agency."
|
||||||
ordering = ["agency"]
|
ordering = ["agency"]
|
||||||
resource_classes = [FederalAgencyResource]
|
resource_classes = [FederalAgencyResource]
|
||||||
|
|
||||||
|
# Readonly fields for analysts and superusers
|
||||||
|
readonly_fields = []
|
||||||
|
|
||||||
|
# Read only that we'll leverage for CISA Analysts
|
||||||
|
analyst_readonly_fields = [] # type: ignore
|
||||||
|
|
||||||
|
# Read only that we'll leverage for OMB Analysts
|
||||||
|
omb_analyst_readonly_fields = [
|
||||||
|
"agency",
|
||||||
|
"federal_type",
|
||||||
|
"acronym",
|
||||||
|
"is_fceb",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Restrict queryset based on user permissions."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return qs.filter(
|
||||||
|
federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return qs # Return full queryset if the user doesn't have the restriction
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Restrict view permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return obj.federal_type == BranchChoices.EXECUTIVE
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
"""Set the read-only state on form elements.
|
||||||
|
We have 2 conditions that determine which fields are read-only:
|
||||||
|
admin user permissions and the domain request creator's status, so
|
||||||
|
we'll use the baseline readonly_fields and extend it as needed.
|
||||||
|
"""
|
||||||
|
readonly_fields = list(self.readonly_fields)
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for OMB analysts
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for analysts and
|
||||||
|
# users who might not belong to groups
|
||||||
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
|
|
||||||
|
|
||||||
class UserGroupAdmin(AuditedAdmin):
|
class UserGroupAdmin(AuditedAdmin):
|
||||||
"""Overwrite the generated UserGroup admin class"""
|
"""Overwrite the generated UserGroup admin class"""
|
||||||
|
@ -4548,11 +5040,11 @@ class WaffleFlagAdmin(FlagAdmin):
|
||||||
return super().changelist_view(request, extra_context=extra_context)
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class DomainGroupAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
list_display = ["name", "portfolio"]
|
list_display = ["name", "portfolio"]
|
||||||
|
|
||||||
|
|
||||||
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class SuborganizationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
|
|
||||||
list_display = ["name", "portfolio"]
|
list_display = ["name", "portfolio"]
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
|
@ -4563,6 +5055,38 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
change_form_template = "django/admin/suborg_change_form.html"
|
change_form_template = "django/admin/suborg_change_form.html"
|
||||||
|
|
||||||
|
readonly_fields = []
|
||||||
|
|
||||||
|
# Even though this is empty, I will leave it as a stub for easy changes in the future
|
||||||
|
# rather than strip it out of our logic.
|
||||||
|
analyst_readonly_fields = [] # type: ignore
|
||||||
|
|
||||||
|
omb_analyst_readonly_fields = [
|
||||||
|
"name",
|
||||||
|
"portfolio",
|
||||||
|
"city",
|
||||||
|
"state_territory",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
"""Set the read-only state on form elements.
|
||||||
|
We have conditions that determine which fields are read-only:
|
||||||
|
admin user permissions and analyst (cisa or omb) status, so
|
||||||
|
we'll use the baseline readonly_fields and extend it as needed.
|
||||||
|
"""
|
||||||
|
readonly_fields = list(self.readonly_fields)
|
||||||
|
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for OMB analysts
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
|
# Return restrictive Read-only fields for analysts and
|
||||||
|
# users who might not belong to groups
|
||||||
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
|
return readonly_fields
|
||||||
|
|
||||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
"""Add suborg's related domains and requests to context"""
|
"""Add suborg's related domains and requests to context"""
|
||||||
obj = self.get_object(request, object_id)
|
obj = self.get_object(request, object_id)
|
||||||
|
@ -4580,6 +5104,30 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
"""Custom get_queryset to filter for OMB analysts."""
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
# Check if user is in OMB analysts group
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return qs.filter(
|
||||||
|
portfolio__organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
portfolio__federal_agency__federal_type=BranchChoices.EXECUTIVE,
|
||||||
|
)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Restrict view permissions based on group membership and model attributes."""
|
||||||
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
return True
|
||||||
|
if obj:
|
||||||
|
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||||
|
return (
|
||||||
|
obj.portfolio
|
||||||
|
and obj.portfolio.federal_agency
|
||||||
|
and obj.portfolio.federal_agency.federal_type == BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
return super().has_view_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
class AllowedEmailAdmin(ListHeaderAdmin):
|
class AllowedEmailAdmin(ListHeaderAdmin):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -105,8 +105,10 @@ export function initApprovedDomain() {
|
||||||
return;
|
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 statusSelect = document.getElementById("id_status");
|
||||||
|
const statusField = document.querySelector("field-status");
|
||||||
const sessionVariableName = "showApprovedDomain";
|
const sessionVariableName = "showApprovedDomain";
|
||||||
let approvedDomainFormGroup = document.querySelector(".field-approved_domain");
|
let approvedDomainFormGroup = document.querySelector(".field-approved_domain");
|
||||||
|
|
||||||
|
@ -120,11 +122,24 @@ export function initApprovedDomain() {
|
||||||
|
|
||||||
// Handle showing/hiding the related fields on page load.
|
// Handle showing/hiding the related fields on page load.
|
||||||
function initializeFormGroups() {
|
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.
|
// Initial handling of these groups.
|
||||||
updateFormGroupVisibility(isStatus);
|
updateFormGroupVisibility(isStatus);
|
||||||
|
|
||||||
|
if (statusSelect) {
|
||||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
statusSelect.addEventListener('change', () => {
|
statusSelect.addEventListener('change', () => {
|
||||||
// Show the approved if the status is what we expect.
|
// Show the approved if the status is what we expect.
|
||||||
|
@ -132,6 +147,7 @@ export function initApprovedDomain() {
|
||||||
updateFormGroupVisibility(isStatus);
|
updateFormGroupVisibility(isStatus);
|
||||||
addOrRemoveSessionBoolean(sessionVariableName, isStatus);
|
addOrRemoveSessionBoolean(sessionVariableName, isStatus);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Listen to Back/Forward button navigation and handle approvedDomainFormGroup display based on session storage
|
// 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
|
// 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 {HTMLElement} modalConfirm - The confirm button in the modal.
|
||||||
* @property {string} apiUrl - The API URL for fetching email content.
|
* @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} 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} sessionVariableName - The session variable name. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||||
* @property {string} apiErrorMessage - The error message that the ajax call returns.
|
* @property {string} apiErrorMessage - The error message that the ajax call returns.
|
||||||
*/
|
*/
|
||||||
|
@ -338,6 +355,7 @@ class CustomizableEmailBase {
|
||||||
this.textAreaFormGroup = config.textAreaFormGroup;
|
this.textAreaFormGroup = config.textAreaFormGroup;
|
||||||
this.dropdownFormGroup = config.dropdownFormGroup;
|
this.dropdownFormGroup = config.dropdownFormGroup;
|
||||||
this.statusToCheck = config.statusToCheck;
|
this.statusToCheck = config.statusToCheck;
|
||||||
|
this.readonlyStatusToCheck = config.readonlyStatusToCheck;
|
||||||
this.sessionVariableName = config.sessionVariableName;
|
this.sessionVariableName = config.sessionVariableName;
|
||||||
|
|
||||||
// Non-configurable variables
|
// Non-configurable variables
|
||||||
|
@ -363,11 +381,22 @@ class CustomizableEmailBase {
|
||||||
|
|
||||||
// Handle showing/hiding the related fields on page load.
|
// Handle showing/hiding the related fields on page load.
|
||||||
initializeFormGroups() {
|
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.
|
// Initial handling of these groups.
|
||||||
this.updateFormGroupVisibility(isStatus);
|
this.updateFormGroupVisibility(isStatus);
|
||||||
|
|
||||||
|
if (this.statusSelect) {
|
||||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
this.statusSelect.addEventListener('change', () => {
|
this.statusSelect.addEventListener('change', () => {
|
||||||
// Show the action needed field if the status is what we expect.
|
// Show the action needed field if the status is what we expect.
|
||||||
|
@ -376,6 +405,7 @@ class CustomizableEmailBase {
|
||||||
this.updateFormGroupVisibility(isStatus);
|
this.updateFormGroupVisibility(isStatus);
|
||||||
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
// 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
|
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||||
|
@ -403,6 +433,7 @@ class CustomizableEmailBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDropdown() {
|
initializeDropdown() {
|
||||||
|
if (this.dropdown) {
|
||||||
this.dropdown.addEventListener("change", () => {
|
this.dropdown.addEventListener("change", () => {
|
||||||
let reason = this.dropdown.value;
|
let reason = this.dropdown.value;
|
||||||
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||||
|
@ -431,8 +462,11 @@ class CustomizableEmailBase {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
initializeModalConfirm() {
|
initializeModalConfirm() {
|
||||||
|
// When the modal confirm button is present, add a listener
|
||||||
|
if (this.modalConfirm) {
|
||||||
this.modalConfirm.addEventListener("click", () => {
|
this.modalConfirm.addEventListener("click", () => {
|
||||||
this.textarea.removeAttribute('readonly');
|
this.textarea.removeAttribute('readonly');
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
|
@ -440,8 +474,11 @@ class CustomizableEmailBase {
|
||||||
hideElement(this.modalTrigger);
|
hideElement(this.modalTrigger);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
initializeDirectEditButton() {
|
initializeDirectEditButton() {
|
||||||
|
// When the direct edit button is present, add a listener
|
||||||
|
if (this.directEditButton) {
|
||||||
this.directEditButton.addEventListener("click", () => {
|
this.directEditButton.addEventListener("click", () => {
|
||||||
this.textarea.removeAttribute('readonly');
|
this.textarea.removeAttribute('readonly');
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
|
@ -449,12 +486,13 @@ class CustomizableEmailBase {
|
||||||
hideElement(this.modalTrigger);
|
hideElement(this.modalTrigger);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isEmailAlreadySent() {
|
isEmailAlreadySent() {
|
||||||
return this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
|
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) {
|
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
|
// 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();
|
this.showPlaceholderNoReason();
|
||||||
|
@ -468,6 +506,7 @@ class CustomizableEmailBase {
|
||||||
|
|
||||||
// Helper function that makes overriding the readonly textarea easy
|
// Helper function that makes overriding the readonly textarea easy
|
||||||
showReadonlyTextarea() {
|
showReadonlyTextarea() {
|
||||||
|
if (this.textarea && this.textareaPlaceholder) {
|
||||||
// A triggering selection is selected, all hands on board:
|
// A triggering selection is selected, all hands on board:
|
||||||
this.textarea.setAttribute('readonly', true);
|
this.textarea.setAttribute('readonly', true);
|
||||||
showElement(this.textarea);
|
showElement(this.textarea);
|
||||||
|
@ -487,6 +526,7 @@ class CustomizableEmailBase {
|
||||||
this.formLabel.innerHTML = "Email:";
|
this.formLabel.innerHTML = "Email:";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function that makes overriding the placeholder reason easy
|
// Helper function that makes overriding the placeholder reason easy
|
||||||
showPlaceholderNoReason() {
|
showPlaceholderNoReason() {
|
||||||
|
@ -516,9 +556,10 @@ class customActionNeededEmail extends CustomizableEmailBase {
|
||||||
lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"),
|
lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"),
|
||||||
modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"),
|
modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"),
|
||||||
apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null,
|
apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null,
|
||||||
textAreaFormGroup: document.querySelector('.field-action_needed_reason'),
|
textAreaFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
||||||
dropdownFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
dropdownFormGroup: document.querySelector('.field-action_needed_reason'),
|
||||||
statusToCheck: "action needed",
|
statusToCheck: "action needed",
|
||||||
|
readonlyStatusToCheck: "Action needed",
|
||||||
sessionVariableName: "showActionNeededReason",
|
sessionVariableName: "showActionNeededReason",
|
||||||
apiErrorMessage: "Error when attempting to grab action needed email: "
|
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
|
// Hide/show the email fields depending on the current status
|
||||||
this.initializeFormGroups();
|
this.initializeFormGroups();
|
||||||
// Setup the textarea, edit button, helper text
|
// 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.initializeDropdown();
|
||||||
this.initializeModalConfirm();
|
this.initializeModalConfirm();
|
||||||
this.initializeDirectEditButton();
|
this.initializeDirectEditButton();
|
||||||
|
@ -560,12 +609,6 @@ export function initActionNeededEmail() {
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
const customEmail = new customActionNeededEmail();
|
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()
|
customEmail.loadActionNeededEmail()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -581,6 +624,7 @@ class customRejectedEmail extends CustomizableEmailBase {
|
||||||
textAreaFormGroup: document.querySelector('.field-rejection_reason'),
|
textAreaFormGroup: document.querySelector('.field-rejection_reason'),
|
||||||
dropdownFormGroup: document.querySelector('.field-rejection_reason_email'),
|
dropdownFormGroup: document.querySelector('.field-rejection_reason_email'),
|
||||||
statusToCheck: "rejected",
|
statusToCheck: "rejected",
|
||||||
|
readonlyStatusToCheck: "Rejected",
|
||||||
sessionVariableName: "showRejectionReason",
|
sessionVariableName: "showRejectionReason",
|
||||||
errorMessage: "Error when attempting to grab rejected email: "
|
errorMessage: "Error when attempting to grab rejected email: "
|
||||||
};
|
};
|
||||||
|
@ -589,7 +633,15 @@ class customRejectedEmail extends CustomizableEmailBase {
|
||||||
|
|
||||||
loadRejectedEmail() {
|
loadRejectedEmail() {
|
||||||
this.initializeFormGroups();
|
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.initializeDropdown();
|
||||||
this.initializeModalConfirm();
|
this.initializeModalConfirm();
|
||||||
this.initializeDirectEditButton();
|
this.initializeDirectEditButton();
|
||||||
|
@ -600,7 +652,7 @@ class customRejectedEmail extends CustomizableEmailBase {
|
||||||
this.showPlaceholder("Email:", "Select a rejection reason to see email");
|
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);
|
super.updateUserInterface(reason, excluded_reasons);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -619,12 +671,6 @@ export function initRejectedEmail() {
|
||||||
|
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
const customEmail = new customRejectedEmail();
|
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()
|
customEmail.loadRejectedEmail()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -648,7 +694,6 @@ function handleSuborgFieldsAndButtons() {
|
||||||
|
|
||||||
// Ensure that every variable is present before proceeding
|
// Ensure that every variable is present before proceeding
|
||||||
if (!requestedSuborganizationField || !suborganizationCity || !suborganizationStateTerritory || !rejectButton) {
|
if (!requestedSuborganizationField || !suborganizationCity || !suborganizationStateTerritory || !rejectButton) {
|
||||||
console.warn("handleSuborganizationSelection() => Could not find required fields.")
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,9 @@ export function handlePortfolioSelection(
|
||||||
suborgDropdownSelector="#id_sub_organization"
|
suborgDropdownSelector="#id_sub_organization"
|
||||||
) {
|
) {
|
||||||
// These dropdown are select2 fields so they must be interacted with via jquery
|
// 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 portfolioDropdown = django.jQuery(portfolioDropdownSelector);
|
||||||
|
const portfolioField = document.querySelector(".field-portfolio");
|
||||||
const suborganizationDropdown = django.jQuery(suborgDropdownSelector);
|
const suborganizationDropdown = django.jQuery(suborgDropdownSelector);
|
||||||
const suborganizationField = document.querySelector(".field-sub_organization");
|
const suborganizationField = document.querySelector(".field-sub_organization");
|
||||||
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
|
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.
|
* - Various global field elements (e.g., `suborganizationField`, `seniorOfficialField`, `portfolioOrgTypeFieldSet`) are used.
|
||||||
*/
|
*/
|
||||||
function updatePortfolioFieldsDisplay() {
|
function updatePortfolioFieldsDisplay() {
|
||||||
|
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
|
// Retrieve the selected portfolio ID
|
||||||
let portfolio_id = portfolioDropdown.val();
|
portfolio_id = portfolioDropdown.val();
|
||||||
|
|
||||||
if (portfolio_id) {
|
if (portfolio_id) {
|
||||||
|
portfolio_selected = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// get readonly field value
|
||||||
|
let portfolio = portfolioField.querySelector(".readonly").innerText;
|
||||||
|
if (portfolio != "-") {
|
||||||
|
portfolio_selected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portfolio_selected) {
|
||||||
// A portfolio is selected - update suborganization dropdown and show/hide relevant fields
|
// A portfolio is selected - update suborganization dropdown and show/hide relevant fields
|
||||||
|
|
||||||
|
if (portfolio_id) {
|
||||||
// Update suborganization dropdown for the selected portfolio
|
// Update suborganization dropdown for the selected portfolio
|
||||||
updateSubOrganizationDropdown(portfolio_id);
|
updateSubOrganizationDropdown(portfolio_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Show fields relevant to a selected portfolio
|
// Show fields relevant to a selected portfolio
|
||||||
showElement(suborganizationField);
|
if (suborganizationField) showElement(suborganizationField);
|
||||||
hideElement(seniorOfficialField);
|
hideElement(seniorOfficialField);
|
||||||
showElement(portfolioSeniorOfficialField);
|
showElement(portfolioSeniorOfficialField);
|
||||||
|
|
||||||
|
@ -427,7 +445,7 @@ export function handlePortfolioSelection(
|
||||||
// No portfolio is selected - reverse visibility of fields
|
// No portfolio is selected - reverse visibility of fields
|
||||||
|
|
||||||
// Hide suborganization field as no portfolio is selected
|
// Hide suborganization field as no portfolio is selected
|
||||||
hideElement(suborganizationField);
|
if (suborganizationField) hideElement(suborganizationField);
|
||||||
|
|
||||||
// Show fields that are relevant when no portfolio is selected
|
// Show fields that are relevant when no portfolio is selected
|
||||||
showElement(seniorOfficialField);
|
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.
|
* This function ensures the form dynamically reflects whether a specific suborganization is being selected or requested.
|
||||||
*/
|
*/
|
||||||
function updateSuborganizationFieldsDisplay() {
|
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();
|
let suborganization_id = suborganizationDropdown.val();
|
||||||
|
|
||||||
if (portfolio_id && !suborganization_id) {
|
if (portfolio_selected && !suborganization_id) {
|
||||||
// Show suborganization request fields
|
// Show suborganization request fields
|
||||||
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
|
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
|
||||||
if (suborganizationCity) showElement(suborganizationCity);
|
if (suborganizationCity) showElement(suborganizationCity);
|
||||||
|
|
|
@ -21,6 +21,8 @@ function handlePortfolioFields(){
|
||||||
const federalTypeField = document.querySelector(".field-federal_type");
|
const federalTypeField = document.querySelector(".field-federal_type");
|
||||||
const urbanizationField = document.querySelector(".field-urbanization");
|
const urbanizationField = document.querySelector(".field-urbanization");
|
||||||
const stateTerritoryDropdown = document.getElementById("id_state_territory");
|
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 seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
|
||||||
const seniorOfficialApi = document.getElementById("senior_official_from_agency_json_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;
|
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
|
* 2. else show org name, hide federal agency, hide federal type if applicable
|
||||||
*/
|
*/
|
||||||
function handleOrganizationTypeChange() {
|
function handleOrganizationTypeChange() {
|
||||||
if (organizationTypeDropdown && organizationNameField) {
|
if (organizationTypeField && organizationNameField) {
|
||||||
let selectedValue = organizationTypeDropdown.value;
|
let selectedValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText;
|
||||||
if (selectedValue === "federal") {
|
if (selectedValue === "federal" || selectedValue === "Federal") {
|
||||||
hideElement(organizationNameField);
|
hideElement(organizationNameField);
|
||||||
showElement(federalAgencyField);
|
showElement(federalAgencyField);
|
||||||
if (federalTypeField) {
|
if (federalTypeField) {
|
||||||
|
@ -207,8 +209,8 @@ function handlePortfolioFields(){
|
||||||
* Handle urbanization
|
* Handle urbanization
|
||||||
*/
|
*/
|
||||||
function handleStateTerritoryChange() {
|
function handleStateTerritoryChange() {
|
||||||
let selectedValue = stateTerritoryDropdown.value;
|
let selectedValue = stateTerritoryDropdown ? stateTerritoryDropdown.value : stateTerritoryReadonly.innerText;
|
||||||
if (selectedValue === "PR") {
|
if (selectedValue === "PR" || selectedValue === "Puerto Rico (PR)") {
|
||||||
showElement(urbanizationField)
|
showElement(urbanizationField)
|
||||||
} else {
|
} else {
|
||||||
hideElement(urbanizationField)
|
hideElement(urbanizationField)
|
||||||
|
@ -265,7 +267,7 @@ function handlePortfolioFields(){
|
||||||
* Initializes necessary data and display configurations for the portfolio fields.
|
* Initializes necessary data and display configurations for the portfolio fields.
|
||||||
*/
|
*/
|
||||||
function initializePortfolioSettings() {
|
function initializePortfolioSettings() {
|
||||||
if (urbanizationField && stateTerritoryDropdown) {
|
if (urbanizationField && stateTerritoryField) {
|
||||||
handleStateTerritoryChange();
|
handleStateTerritoryChange();
|
||||||
}
|
}
|
||||||
handleOrganizationTypeChange();
|
handleOrganizationTypeChange();
|
||||||
|
@ -285,10 +287,12 @@ function handlePortfolioFields(){
|
||||||
handleStateTerritoryChange();
|
handleStateTerritoryChange();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (organizationTypeDropdown) {
|
||||||
organizationTypeDropdown.addEventListener("change", function() {
|
organizationTypeDropdown.addEventListener("change", function() {
|
||||||
handleOrganizationTypeChange();
|
handleOrganizationTypeChange();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run initial setup functions
|
// Run initial setup functions
|
||||||
initializePortfolioSettings();
|
initializePortfolioSettings();
|
||||||
|
|
|
@ -12,6 +12,9 @@ logger = logging.getLogger(__name__)
|
||||||
# Constants for clarity
|
# Constants for clarity
|
||||||
ALL = "all"
|
ALL = "all"
|
||||||
IS_STAFF = "is_staff"
|
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_MANAGER = "is_domain_manager"
|
||||||
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||||
|
@ -108,6 +111,9 @@ def _user_has_permission(user, request, rules, **kwargs):
|
||||||
# Define permission checks
|
# Define permission checks
|
||||||
permission_checks = [
|
permission_checks = [
|
||||||
(IS_STAFF, lambda: user.is_staff),
|
(IS_STAFF, lambda: user.is_staff),
|
||||||
|
(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,
|
IS_DOMAIN_MANAGER,
|
||||||
lambda: (not user.is_org_user(request) and _is_domain_manager(user, **kwargs))
|
lambda: (not user.is_org_user(request) and _is_domain_manager(user, **kwargs))
|
||||||
|
|
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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -971,6 +971,24 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
logger.info("making technical contact")
|
logger.info("making technical contact")
|
||||||
self._set_singleton_contact(contact, expectedType=contact.ContactTypeChoices.TECHNICAL)
|
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:
|
def is_active(self) -> bool:
|
||||||
"""Currently just returns if the state is created,
|
"""Currently just returns if the state is created,
|
||||||
because then it should be live, theoretically.
|
because then it should be live, theoretically.
|
||||||
|
@ -1678,24 +1696,26 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
return help_text
|
return help_text
|
||||||
|
|
||||||
def _disclose_fields(self, contact: PublicContact):
|
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.
|
.disclose= <this function> on the command before sending.
|
||||||
if item is security email then make sure email is visible"""
|
if item is security email then make sure email is visible"""
|
||||||
# You can find each enum here:
|
# You can find each enum here:
|
||||||
# https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32
|
# https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32
|
||||||
DF = epp.DiscloseField
|
DF = epp.DiscloseField
|
||||||
excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT}
|
all_disclose_fields = {field for field in DF}
|
||||||
all_disclose_fields = {field for field in DF} - excluded_disclose_fields
|
disclose_args = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}}
|
||||||
disclose_fields = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}}
|
|
||||||
if contact.contact_type == contact.ContactTypeChoices.SECURITY and contact.email not in [
|
|
||||||
email for email in DefaultEmail
|
|
||||||
]:
|
|
||||||
disclose_fields["fields"] -= {DF.EMAIL}
|
|
||||||
elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE:
|
|
||||||
disclose_fields["fields"] -= {DF.EMAIL, DF.VOICE, DF.ADDR}
|
|
||||||
|
|
||||||
logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag"))
|
fields_to_remove = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT}
|
||||||
return epp.Disclose(**disclose_fields) # type: ignore
|
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
|
def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore
|
||||||
return epp.PostalInfo( # type: ignore
|
return epp.PostalInfo( # type: ignore
|
||||||
|
|
|
@ -449,7 +449,9 @@ class DomainInformation(TimeStampedModel):
|
||||||
def converted_federal_type(self):
|
def converted_federal_type(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.federal_type
|
return self.portfolio.federal_type
|
||||||
return self.federal_type
|
elif self.federal_agency:
|
||||||
|
return self.federal_agency.federal_type
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_senior_official(self):
|
def converted_senior_official(self):
|
||||||
|
|
|
@ -1505,7 +1505,9 @@ class DomainRequest(TimeStampedModel):
|
||||||
def converted_federal_type(self):
|
def converted_federal_type(self):
|
||||||
if self.portfolio:
|
if self.portfolio:
|
||||||
return self.portfolio.federal_type
|
return self.portfolio.federal_type
|
||||||
return self.federal_type
|
elif self.federal_agency:
|
||||||
|
return self.federal_agency.federal_type
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def converted_address_line1(self):
|
def converted_address_line1(self):
|
||||||
|
|
|
@ -76,33 +76,6 @@ class PublicContact(TimeStampedModel):
|
||||||
)
|
)
|
||||||
pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.")
|
pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""Save to the registry and also locally in the registrar database."""
|
|
||||||
skip_epp_save = kwargs.pop("skip_epp_save", False)
|
|
||||||
if hasattr(self, "domain") and not skip_epp_save:
|
|
||||||
self.add_to_domain_in_epp()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def add_to_domain_in_epp(self):
|
|
||||||
"""Adds the current contact to the underlying domain in EPP."""
|
|
||||||
match self.contact_type:
|
|
||||||
case PublicContact.ContactTypeChoices.REGISTRANT:
|
|
||||||
self.domain.registrant_contact = self
|
|
||||||
case PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
|
||||||
self.domain.administrative_contact = self
|
|
||||||
case PublicContact.ContactTypeChoices.TECHNICAL:
|
|
||||||
self.domain.technical_contact = self
|
|
||||||
case PublicContact.ContactTypeChoices.SECURITY:
|
|
||||||
self.domain.security_contact = self
|
|
||||||
|
|
||||||
def print_contact_info_epp(self):
|
|
||||||
"""Prints registry data for this PublicContact for easier debugging"""
|
|
||||||
results = self.domain._request_contact_info(self, get_result_as_dict=True)
|
|
||||||
logger.info("Contact Info from EPP:")
|
|
||||||
logger.info("=====================")
|
|
||||||
for key, value in results.items():
|
|
||||||
logger.info(f"{key}: {value}")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_registrant(cls):
|
def get_default_registrant(cls):
|
||||||
return cls(
|
return cls(
|
||||||
|
|
|
@ -141,6 +141,99 @@ class UserGroup(Group):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating analyst permissions group: {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):
|
def create_full_access_group(apps, schema_editor):
|
||||||
"""This method gets run from a data migration."""
|
"""This method gets run from a data migration."""
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if perms.registrar.analyst_access_permission or perms.full_access_permission %}
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<table class="width-full">
|
<table class="width-full">
|
||||||
<caption class="text-bold">Analytics</caption>
|
<caption class="text-bold">Analytics</caption>
|
||||||
|
@ -78,6 +79,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -11,13 +11,15 @@
|
||||||
{% block field_sets %}
|
{% block field_sets %}
|
||||||
<div class="display-flex flex-row flex-justify submit-row">
|
<div class="display-flex flex-row flex-justify submit-row">
|
||||||
<div class="flex-align-self-start button-list-mobile">
|
<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">
|
<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. #}
|
{# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
|
||||||
<span class="mini-spacer"></span>
|
<span class="mini-spacer"></span>
|
||||||
<input type="submit" value="Get registry status" name="_get_status">
|
<input type="submit" value="Get registry status" name="_get_status">
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop:flex-align-self-end">
|
<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>
|
<a class="text-middle" href="#toggle-extend-expiration-alert" aria-controls="toggle-extend-expiration-alert" data-open-modal>
|
||||||
Extend expiration date
|
Extend expiration date
|
||||||
</a>
|
</a>
|
||||||
|
@ -31,9 +33,11 @@
|
||||||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
{% 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>
|
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||||
{% endif %}
|
{% 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>
|
<a class="text-middle" href="#toggle-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal>
|
||||||
Remove from registry
|
Remove from registry
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
|
|
||||||
{% if show_formatted_name %}
|
{% if show_formatted_name %}
|
||||||
{% if user.get_formatted_name %}
|
{% if user.get_formatted_name %}
|
||||||
|
{% 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>
|
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
None
|
None
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -69,7 +69,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% elif field.field.name == "portfolio_senior_official" %}
|
{% elif field.field.name == "portfolio_senior_official" %}
|
||||||
<div class="readonly">
|
<div class="readonly">
|
||||||
{% if original_object.portfolio.senior_official %}
|
{% if original_object.portfolio.senior_official %}
|
||||||
|
{% 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>
|
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
No senior official found.<br>
|
No senior official found.<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -78,7 +82,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% if all_contacts.count > 2 %}
|
{% if all_contacts.count > 2 %}
|
||||||
<div class="readonly">
|
<div class="readonly">
|
||||||
{% for contact in all_contacts %}
|
{% for contact in all_contacts %}
|
||||||
|
{% 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 %}
|
<a href="{% url 'admin:registrar_contact_change' contact.id %}">{{ contact.get_formatted_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -153,6 +161,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
<p>No additional members found.</p>
|
<p>No additional members found.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 %}
|
{% else %}
|
||||||
<div class="readonly">{{ field.contents }}</div>
|
<div class="readonly">{{ field.contents }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -16,7 +16,11 @@
|
||||||
{% for admin in admins %}
|
{% for admin in admins %}
|
||||||
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
|
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
|
||||||
<tr>
|
<tr>
|
||||||
|
{% 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>
|
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||||
|
{% endif %}
|
||||||
<td>{{ admin.user.title }}</td>
|
<td>{{ admin.user.title }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if admin.user.email %}
|
{% if admin.user.email %}
|
||||||
|
|
|
@ -30,6 +30,9 @@
|
||||||
<a href={{ url }}>No senior official found. Create one now.</a>
|
<a href={{ url }}>No senior official found. Create one now.</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
|
||||||
|
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="readonly">{{ field.contents }}</div>
|
<div class="readonly">{{ field.contents }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -5,7 +5,7 @@ A domain manager was removed from {{ domain.name }}.
|
||||||
|
|
||||||
REMOVED BY: {{ removed_by.email }}
|
REMOVED BY: {{ removed_by.email }}
|
||||||
REMOVED ON: {{ date }}
|
REMOVED ON: {{ date }}
|
||||||
MANAGER REMOVED: {{ manager_removed.email }}
|
MANAGER REMOVED: {{ manager_removed_email }}
|
||||||
|
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -1010,6 +1010,27 @@ def create_user(**kwargs):
|
||||||
return user
|
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():
|
def create_test_user():
|
||||||
username = "test_user"
|
username = "test_user"
|
||||||
first_name = "First"
|
first_name = "First"
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.utils import timezone
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from registrar import models
|
from registrar import models
|
||||||
|
from registrar.utility.constants import BranchChoices
|
||||||
from registrar.utility.email import EmailSendingError
|
from registrar.utility.email import EmailSendingError
|
||||||
from registrar.utility.errors import MissingEmailError
|
from registrar.utility.errors import MissingEmailError
|
||||||
from waffle.testutils import override_flag
|
from waffle.testutils import override_flag
|
||||||
|
@ -57,6 +58,7 @@ from .common import (
|
||||||
MockDbForSharedTests,
|
MockDbForSharedTests,
|
||||||
AuditedAdminMockData,
|
AuditedAdminMockData,
|
||||||
completed_domain_request,
|
completed_domain_request,
|
||||||
|
create_omb_analyst_user,
|
||||||
create_test_user,
|
create_test_user,
|
||||||
generic_domain_object,
|
generic_domain_object,
|
||||||
less_console_noise,
|
less_console_noise,
|
||||||
|
@ -136,18 +138,25 @@ class TestDomainInvitationAdmin(WebTest):
|
||||||
csrf_checks = False
|
csrf_checks = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
self.site = AdminSite()
|
cls.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
self.superuser = create_superuser()
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.cisa_analyst = create_user()
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
|
self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
|
||||||
self.domain = Domain.objects.create(name="example.com")
|
self.domain = Domain.objects.create(name="example.com")
|
||||||
|
self.fed_agency = FederalAgency.objects.create(
|
||||||
|
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
self.portfolio = Portfolio.objects.create(organization_name="new portfolio", creator=self.superuser)
|
self.portfolio = Portfolio.objects.create(organization_name="new portfolio", creator=self.superuser)
|
||||||
DomainInformation.objects.create(domain=self.domain, portfolio=self.portfolio, creator=self.superuser)
|
self.domain_info = DomainInformation.objects.create(
|
||||||
|
domain=self.domain, portfolio=self.portfolio, creator=self.superuser
|
||||||
|
)
|
||||||
"""Create a client object"""
|
"""Create a client object"""
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
@ -159,10 +168,124 @@ class TestDomainInvitationAdmin(WebTest):
|
||||||
DomainInvitation.objects.all().delete()
|
DomainInvitation.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
|
self.fed_agency.delete()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_view(self):
|
||||||
|
"""Ensure regular analysts can view domain invitations."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
self.client.force_login(self.cisa_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, invitation.email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view_non_feb_domain(self):
|
||||||
|
"""Ensure OMB analysts cannot view non-federal domains."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||||
|
self.assertNotContains(response, invitation.email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view_feb_domain(self):
|
||||||
|
"""Ensure OMB analysts can view federal executive branch domains."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
self.portfolio.organization_type = DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
self.portfolio.federal_agency = self.fed_agency
|
||||||
|
self.portfolio.save()
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||||
|
self.assertContains(response, invitation.email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_view(self):
|
||||||
|
"""Ensure superusers can view domain invitations."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, invitation.email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_change(self):
|
||||||
|
"""Ensure regular analysts can view domain invitations but not update."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
self.client.force_login(self.cisa_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, invitation.email)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertNotContains(response, "id_domain")
|
||||||
|
self.assertNotContains(response, "id_email")
|
||||||
|
self.assertContains(response, "closelink")
|
||||||
|
self.assertNotContains(response, "Save")
|
||||||
|
self.assertNotContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change_non_feb_domain(self):
|
||||||
|
"""Ensure OMB analysts cannot change non-federal domains."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change_feb_domain(self):
|
||||||
|
"""Ensure OMB analysts can view federal executive branch domains."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
# update domain
|
||||||
|
self.portfolio.organization_type = DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
self.portfolio.federal_agency = self.fed_agency
|
||||||
|
self.portfolio.save()
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, invitation.email)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertNotContains(response, "id_domain")
|
||||||
|
self.assertNotContains(response, "id_email")
|
||||||
|
self.assertContains(response, "closelink")
|
||||||
|
self.assertNotContains(response, "Save")
|
||||||
|
self.assertNotContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_change(self):
|
||||||
|
"""Ensure superusers can change domain invitations."""
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, invitation.email)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertContains(response, "id_domain")
|
||||||
|
self.assertContains(response, "id_email")
|
||||||
|
self.assertNotContains(response, "closelink")
|
||||||
|
self.assertContains(response, "Save")
|
||||||
|
self.assertContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_filter_feb_domain(self):
|
||||||
|
"""Ensure OMB analysts can apply filters and only federal executive branch domains show."""
|
||||||
|
# create invitation on domain that is not FEB
|
||||||
|
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("admin:registrar_domaininvitation_changelist"),
|
||||||
|
{"status": DomainInvitation.DomainInvitationStatus.INVITED},
|
||||||
|
)
|
||||||
|
self.assertNotContains(response, invitation.email)
|
||||||
|
# update domain
|
||||||
|
self.portfolio.organization_type = DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
self.portfolio.federal_agency = self.fed_agency
|
||||||
|
self.portfolio.save()
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("admin:registrar_domaininvitation_changelist"),
|
||||||
|
{"status": DomainInvitation.DomainInvitationStatus.INVITED},
|
||||||
|
)
|
||||||
|
self.assertContains(response, invitation.email)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -1139,6 +1262,7 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
self.testuser = create_test_user()
|
self.testuser = create_test_user()
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -1148,6 +1272,26 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
UserPortfolioPermission.objects.all().delete()
|
UserPortfolioPermission.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view user portfolio permissions list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_userportfoliopermission_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change(self):
|
||||||
|
"""Ensure OMB analysts cannot change user portfolio permission."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.superuser, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/userportfoliopermission/{}/change/".format(user_portfolio_permission.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_change_form_description(self):
|
def test_has_change_form_description(self):
|
||||||
"""Tests if this model has a model description on the change form view"""
|
"""Tests if this model has a model description on the change form view"""
|
||||||
|
@ -1204,6 +1348,7 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Create a client object"""
|
"""Create a client object"""
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -1217,6 +1362,26 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
def tearDownClass(self):
|
def tearDownClass(self):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view portfolio invitations list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolioinvitation_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change(self):
|
||||||
|
"""Ensure OMB analysts cannot change portfolio invitation."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||||
|
email=self.superuser.email, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/portfolioinvitation/{}/change/".format(invitation.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -1791,6 +1956,8 @@ class TestHostAdmin(TestCase):
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
cls.admin = MyHostAdmin(model=Host, admin_site=cls.site)
|
cls.admin = MyHostAdmin(model=Host, admin_site=cls.site)
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
|
cls.staffuser = create_user()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Setup environment for a mock admin user"""
|
"""Setup environment for a mock admin user"""
|
||||||
|
@ -1806,6 +1973,20 @@ class TestHostAdmin(TestCase):
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_view(self):
|
||||||
|
"""Ensure analysts cannot view hosts list."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_host_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view hosts list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_host_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -1870,6 +2051,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
cls.admin = DomainInformationAdmin(model=DomainInformation, admin_site=cls.site)
|
cls.admin = DomainInformationAdmin(model=DomainInformation, admin_site=cls.site)
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
cls.staffuser = create_user()
|
cls.staffuser = create_user()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
cls.mock_data_generator = AuditedAdminMockData()
|
cls.mock_data_generator = AuditedAdminMockData()
|
||||||
cls.test_helper = GenericTestHelper(
|
cls.test_helper = GenericTestHelper(
|
||||||
factory=cls.factory,
|
factory=cls.factory,
|
||||||
|
@ -1881,12 +2063,24 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.nonfeddomain = Domain.objects.create(name="nonfeddomain.com")
|
||||||
|
self.feddomain = Domain.objects.create(name="feddomain.com")
|
||||||
|
self.fed_agency = FederalAgency.objects.create(
|
||||||
|
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
self.portfolio = Portfolio.objects.create(organization_name="new portfolio", creator=self.superuser)
|
||||||
|
self.domain_info = DomainInformation.objects.create(
|
||||||
|
domain=self.feddomain, portfolio=self.portfolio, creator=self.superuser
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Delete all Users, Domains, and UserDomainRoles"""
|
"""Delete all Users, Domains, and UserDomainRoles"""
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
self.fed_agency.delete()
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1894,6 +2088,56 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
SeniorOfficial.objects.all().delete()
|
SeniorOfficial.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_view(self):
|
||||||
|
"""Ensure regular analysts cannot view domain information list."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininformation_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view domain information list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininformation_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_view(self):
|
||||||
|
"""Ensure superusers can view domain information list."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_domaininformation_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feddomain.name)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_change(self):
|
||||||
|
"""Ensure regular analysts cannot view/edit domain information directly."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("admin:registrar_domaininformation_change", args=[self.feddomain.domain_info.id])
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change(self):
|
||||||
|
"""Ensure OMB analysts cannot view/edit domain information directly."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("admin:registrar_domaininformation_change", args=[self.feddomain.domain_info.id])
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_change(self):
|
||||||
|
"""Ensure superusers can view/change domain information directly."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("admin:registrar_domaininformation_change", args=[self.feddomain.domain_info.id])
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feddomain.name)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_information_senior_official_is_alphabetically_sorted(self):
|
def test_domain_information_senior_official_is_alphabetically_sorted(self):
|
||||||
"""Tests if the senior offical dropdown is alphanetically sorted in the django admin display"""
|
"""Tests if the senior offical dropdown is alphanetically sorted in the django admin display"""
|
||||||
|
@ -2258,6 +2502,8 @@ class TestUserDomainRoleAdmin(WebTest):
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
cls.admin = UserDomainRoleAdmin(model=UserDomainRole, admin_site=cls.site)
|
cls.admin = UserDomainRoleAdmin(model=UserDomainRole, admin_site=cls.site)
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
|
cls.staffuser = create_user()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
cls.test_helper = GenericTestHelper(
|
cls.test_helper = GenericTestHelper(
|
||||||
factory=cls.factory,
|
factory=cls.factory,
|
||||||
user=cls.superuser,
|
user=cls.superuser,
|
||||||
|
@ -2285,6 +2531,31 @@ class TestUserDomainRoleAdmin(WebTest):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_view(self):
|
||||||
|
"""Ensure analysts cannot view user domain roles list."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_userdomainrole_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view user domain roles list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_userdomainrole_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change(self):
|
||||||
|
"""Ensure OMB analysts cannot view/edit user domain roles list."""
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="anyrandomdomain.com")
|
||||||
|
user_domain_role, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.superuser, domain=domain, role=[UserDomainRole.Roles.MANAGER]
|
||||||
|
)
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_userdomainrole_change", args=[user_domain_role.id]))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -2580,6 +2851,7 @@ class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
||||||
cls.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
cls.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
cls.staffuser = create_user()
|
cls.staffuser = create_user()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -2596,6 +2868,13 @@ class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view users list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_user_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -3221,6 +3500,7 @@ class TestContactAdmin(TestCase):
|
||||||
cls.admin = ContactAdmin(model=Contact, admin_site=None)
|
cls.admin = ContactAdmin(model=Contact, admin_site=None)
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
cls.staffuser = create_user()
|
cls.staffuser = create_user()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
@ -3236,6 +3516,13 @@ class TestContactAdmin(TestCase):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view contact list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_contact_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -3282,6 +3569,7 @@ class TestVerifiedByStaffAdmin(TestCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
cls.site = AdminSite()
|
cls.site = AdminSite()
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
cls.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=cls.site)
|
cls.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=cls.site)
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||||
|
@ -3299,18 +3587,20 @@ class TestVerifiedByStaffAdmin(TestCase):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view verified by staff list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_verifiedbystaff_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("admin:registrar_verifiedbystaff_changelist"))
|
||||||
"/admin/registrar/verifiedbystaff/",
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure that the page is loaded correctly
|
# Make sure that the page is loaded correctly
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Test for a description snippet
|
# Test for a description snippet
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response, "This table contains users who have been allowed to bypass " "identity proofing through Login.gov"
|
response, "This table contains users who have been allowed to bypass " "identity proofing through Login.gov"
|
||||||
|
@ -3365,6 +3655,7 @@ class TestWebsiteAdmin(TestCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.admin = WebsiteAdmin(model=Website, admin_site=self.site)
|
self.admin = WebsiteAdmin(model=Website, admin_site=self.site)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
@ -3375,15 +3666,18 @@ class TestWebsiteAdmin(TestCase):
|
||||||
Website.objects.all().delete()
|
Website.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view website list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_website_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("admin:registrar_website_changelist"))
|
||||||
"/admin/registrar/website/",
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure that the page is loaded correctly
|
# Make sure that the page is loaded correctly
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -3392,13 +3686,14 @@ class TestWebsiteAdmin(TestCase):
|
||||||
self.assertContains(response, "Show more")
|
self.assertContains(response, "Show more")
|
||||||
|
|
||||||
|
|
||||||
class TestDraftDomain(TestCase):
|
class TestDraftDomainAdmin(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
cls.site = AdminSite()
|
cls.site = AdminSite()
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
cls.admin = DraftDomainAdmin(model=DraftDomain, admin_site=cls.site)
|
cls.admin = DraftDomainAdmin(model=DraftDomain, admin_site=cls.site)
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||||
|
@ -3416,15 +3711,18 @@ class TestDraftDomain(TestCase):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view draft domain list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_draftdomain_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("admin:registrar_draftdomain_changelist"))
|
||||||
"/admin/registrar/draftdomain/",
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure that the page is loaded correctly
|
# Make sure that the page is loaded correctly
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -3435,13 +3733,21 @@ class TestDraftDomain(TestCase):
|
||||||
self.assertContains(response, "Show more")
|
self.assertContains(response, "Show more")
|
||||||
|
|
||||||
|
|
||||||
class TestFederalAgency(TestCase):
|
class TestFederalAgencyAdmin(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
cls.site = AdminSite()
|
cls.site = AdminSite()
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
|
cls.staffuser = create_user()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
|
cls.non_feb_agency = FederalAgency.objects.create(
|
||||||
|
agency="Fake judicial agency", federal_type=BranchChoices.JUDICIAL
|
||||||
|
)
|
||||||
|
cls.feb_agency = FederalAgency.objects.create(
|
||||||
|
agency="Fake executive agency", federal_type=BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
cls.admin = FederalAgencyAdmin(model=FederalAgency, admin_site=cls.site)
|
cls.admin = FederalAgencyAdmin(model=FederalAgency, admin_site=cls.site)
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||||
|
@ -3454,6 +3760,100 @@ class TestFederalAgency(TestCase):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_view(self):
|
||||||
|
"""Ensure regular analysts can view federal agencies."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.non_feb_agency.agency)
|
||||||
|
self.assertContains(response, self.feb_agency.agency)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts can view FEB agencies but not other branches."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotContains(response, self.non_feb_agency.agency)
|
||||||
|
self.assertContains(response, self.feb_agency.agency)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_view(self):
|
||||||
|
"""Ensure superusers can view domain invitations."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.non_feb_agency.agency)
|
||||||
|
self.assertContains(response, self.feb_agency.agency)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_change(self):
|
||||||
|
"""Ensure regular analysts can view/edit federal agencies list."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.non_feb_agency.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.feb_agency.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feb_agency.agency)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertContains(response, "id_agency")
|
||||||
|
self.assertContains(response, "id_federal_type")
|
||||||
|
self.assertContains(response, "id_acronym")
|
||||||
|
self.assertContains(response, "id_is_fceb")
|
||||||
|
self.assertNotContains(response, "closelink")
|
||||||
|
self.assertContains(response, "Save")
|
||||||
|
self.assertContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change(self):
|
||||||
|
"""Ensure OMB analysts can change FEB agencies but not others."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.non_feb_agency.id]))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.feb_agency.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feb_agency.agency)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertNotContains(response, "id_agency")
|
||||||
|
self.assertNotContains(response, "id_federal_type")
|
||||||
|
self.assertNotContains(response, "id_acronym")
|
||||||
|
self.assertNotContains(response, "id_is_fceb")
|
||||||
|
self.assertContains(response, "closelink")
|
||||||
|
self.assertNotContains(response, "Save")
|
||||||
|
self.assertNotContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_change(self):
|
||||||
|
"""Ensure superusers can change all federal agencies."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.non_feb_agency.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.feb_agency.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feb_agency.agency)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertContains(response, "id_agency")
|
||||||
|
self.assertContains(response, "id_federal_type")
|
||||||
|
self.assertContains(response, "id_acronym")
|
||||||
|
self.assertContains(response, "id_is_fceb")
|
||||||
|
self.assertNotContains(response, "closelink")
|
||||||
|
self.assertContains(response, "Save")
|
||||||
|
self.assertContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_filter_feb_agencies(self):
|
||||||
|
"""Ensure OMB analysts can apply filters and only federal agencies show."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
# in setup, created two agencies: Fake judicial agency and Fake executive agency
|
||||||
|
# only executive agency should show up with the search for 'fake'
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("admin:registrar_federalagency_changelist"),
|
||||||
|
data={"q": "fake"},
|
||||||
|
)
|
||||||
|
self.assertNotContains(response, self.non_feb_agency.agency)
|
||||||
|
self.assertContains(response, self.feb_agency.agency)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -3471,11 +3871,12 @@ class TestFederalAgency(TestCase):
|
||||||
self.assertContains(response, "Show more")
|
self.assertContains(response, "Show more")
|
||||||
|
|
||||||
|
|
||||||
class TestPublicContact(TestCase):
|
class TestPublicContactAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.admin = PublicContactAdmin(model=PublicContact, admin_site=self.site)
|
self.admin = PublicContactAdmin(model=PublicContact, admin_site=self.site)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
@ -3486,16 +3887,19 @@ class TestPublicContact(TestCase):
|
||||||
PublicContact.objects.all().delete()
|
PublicContact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view public contact list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_publiccontact_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("admin:registrar_publiccontact_changelist"))
|
||||||
"/admin/registrar/publiccontact/",
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure that the page is loaded correctly
|
# Make sure that the page is loaded correctly
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -3504,11 +3908,12 @@ class TestPublicContact(TestCase):
|
||||||
self.assertContains(response, "Show more")
|
self.assertContains(response, "Show more")
|
||||||
|
|
||||||
|
|
||||||
class TestTransitionDomain(TestCase):
|
class TestTransitionDomainAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.admin = TransitionDomainAdmin(model=TransitionDomain, admin_site=self.site)
|
self.admin = TransitionDomainAdmin(model=TransitionDomain, admin_site=self.site)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
@ -3519,15 +3924,18 @@ class TestTransitionDomain(TestCase):
|
||||||
PublicContact.objects.all().delete()
|
PublicContact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view transition domain list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_transitiondomain_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("admin:registrar_transitiondomain_changelist"))
|
||||||
"/admin/registrar/transitiondomain/",
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure that the page is loaded correctly
|
# Make sure that the page is loaded correctly
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -3536,11 +3944,12 @@ class TestTransitionDomain(TestCase):
|
||||||
self.assertContains(response, "Show more")
|
self.assertContains(response, "Show more")
|
||||||
|
|
||||||
|
|
||||||
class TestUserGroup(TestCase):
|
class TestUserGroupAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.admin = UserGroupAdmin(model=UserGroup, admin_site=self.site)
|
self.admin = UserGroupAdmin(model=UserGroup, admin_site=self.site)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
@ -3550,15 +3959,18 @@ class TestUserGroup(TestCase):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts cannot view user group list."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_usergroup_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
response = self.client.get(
|
response = self.client.get(reverse("admin:registrar_usergroup_changelist"))
|
||||||
"/admin/registrar/usergroup/",
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure that the page is loaded correctly
|
# Make sure that the page is loaded correctly
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -3575,12 +3987,23 @@ class TestPortfolioAdmin(TestCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
cls.site = AdminSite()
|
cls.site = AdminSite()
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
|
cls.staffuser = create_user()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
self.portfolio = Portfolio.objects.create(organization_name="Test portfolio", creator=self.superuser)
|
||||||
|
self.feb_agency = FederalAgency.objects.create(
|
||||||
|
agency="Test FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||||
|
)
|
||||||
|
self.feb_portfolio = Portfolio.objects.create(
|
||||||
|
organization_name="Test FEB portfolio",
|
||||||
|
creator=self.superuser,
|
||||||
|
federal_agency=self.feb_agency,
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
Suborganization.objects.all().delete()
|
Suborganization.objects.all().delete()
|
||||||
|
@ -3588,8 +4011,118 @@ class TestPortfolioAdmin(TestCase):
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
|
self.feb_agency.delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_view(self):
|
||||||
|
"""Ensure regular analysts can view portfolios."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.portfolio.organization_name)
|
||||||
|
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_view(self):
|
||||||
|
"""Ensure OMB analysts can view FEB portfolios but not others."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotContains(response, self.portfolio.organization_name)
|
||||||
|
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_view(self):
|
||||||
|
"""Ensure superusers can view portfolios."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_changelist"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.portfolio.organization_name)
|
||||||
|
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_analyst_change(self):
|
||||||
|
"""Ensure regular analysts can view/edit portfolios."""
|
||||||
|
self.client.force_login(self.staffuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.portfolio.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.feb_portfolio.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertContains(response, "id_organization_name")
|
||||||
|
self.assertContains(response, "id_notes")
|
||||||
|
self.assertContains(response, "id_organization_type")
|
||||||
|
self.assertContains(response, "id_state_territory")
|
||||||
|
self.assertContains(response, "id_address_line1")
|
||||||
|
self.assertContains(response, "id_address_line2")
|
||||||
|
self.assertContains(response, "id_city")
|
||||||
|
self.assertContains(response, "id_zipcode")
|
||||||
|
self.assertContains(response, "id_urbanization")
|
||||||
|
self.assertNotContains(response, "closelink")
|
||||||
|
self.assertContains(response, "Save")
|
||||||
|
self.assertContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_change(self):
|
||||||
|
"""Ensure OMB analysts can change FEB portfolios but not others."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.portfolio.id]))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.feb_portfolio.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertNotContains(response, "id_organization_name")
|
||||||
|
self.assertNotContains(response, "id_notes")
|
||||||
|
self.assertNotContains(response, "id_organization_type")
|
||||||
|
self.assertNotContains(response, "id_state_territory")
|
||||||
|
self.assertNotContains(response, "id_address_line1")
|
||||||
|
self.assertNotContains(response, "id_address_line2")
|
||||||
|
self.assertNotContains(response, "id_city")
|
||||||
|
self.assertNotContains(response, "id_zipcode")
|
||||||
|
self.assertNotContains(response, "id_urbanization")
|
||||||
|
self.assertContains(response, "closelink")
|
||||||
|
self.assertNotContains(response, "Save")
|
||||||
|
self.assertNotContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_superuser_change(self):
|
||||||
|
"""Ensure superusers can change all portfolios."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.portfolio.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.feb_portfolio.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||||
|
# test whether fields are readonly or editable
|
||||||
|
self.assertContains(response, "id_organization_name")
|
||||||
|
self.assertContains(response, "id_notes")
|
||||||
|
self.assertContains(response, "id_organization_type")
|
||||||
|
self.assertContains(response, "id_state_territory")
|
||||||
|
self.assertContains(response, "id_address_line1")
|
||||||
|
self.assertContains(response, "id_address_line2")
|
||||||
|
self.assertContains(response, "id_city")
|
||||||
|
self.assertContains(response, "id_zipcode")
|
||||||
|
self.assertContains(response, "id_urbanization")
|
||||||
|
self.assertNotContains(response, "closelink")
|
||||||
|
self.assertContains(response, "Save")
|
||||||
|
self.assertContains(response, "Delete")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst_filter_feb_portfolios(self):
|
||||||
|
"""Ensure OMB analysts can apply filters and only feb portfolios show."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
# in setup, created two portfolios: Test portfolio and Test FEB portfolio
|
||||||
|
# only executive portfolio should show up with the search for 'portfolio'
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("admin:registrar_portfolio_changelist"),
|
||||||
|
data={"q": "test"},
|
||||||
|
)
|
||||||
|
self.assertNotContains(response, self.portfolio.organization_name)
|
||||||
|
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_created_on_display(self):
|
def test_created_on_display(self):
|
||||||
"""Tests the custom created on which is a reskin of the created_at field"""
|
"""Tests the custom created on which is a reskin of the created_at field"""
|
||||||
|
@ -3777,6 +4310,7 @@ class TestTransferUser(WebTest):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
cls.site = AdminSite()
|
cls.site = AdminSite()
|
||||||
cls.superuser = create_superuser()
|
cls.superuser = create_superuser()
|
||||||
|
cls.omb_analyst = create_omb_analyst_user()
|
||||||
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
|
|
||||||
|
@ -3797,6 +4331,13 @@ class TestTransferUser(WebTest):
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_omb_analyst(self):
|
||||||
|
"""Ensure OMB analysts cannot view transfer_user."""
|
||||||
|
self.client.force_login(self.omb_analyst)
|
||||||
|
response = self.client.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_transfer_user_shows_current_and_selected_user_information(self):
|
def test_transfer_user_shows_current_and_selected_user_information(self):
|
||||||
"""Assert we pull the current user info and display it on the transfer page"""
|
"""Assert we pull the current user info and display it on the transfer page"""
|
||||||
|
|
|
@ -17,14 +17,17 @@ from registrar.models import (
|
||||||
Host,
|
Host,
|
||||||
Portfolio,
|
Portfolio,
|
||||||
)
|
)
|
||||||
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.public_contact import PublicContact
|
from registrar.models.public_contact import PublicContact
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
from registrar.utility.constants import BranchChoices
|
||||||
from .common import (
|
from .common import (
|
||||||
MockSESClient,
|
MockSESClient,
|
||||||
completed_domain_request,
|
completed_domain_request,
|
||||||
less_console_noise,
|
less_console_noise,
|
||||||
create_superuser,
|
create_superuser,
|
||||||
create_user,
|
create_user,
|
||||||
|
create_omb_analyst_user,
|
||||||
create_ready_domain,
|
create_ready_domain,
|
||||||
MockEppLib,
|
MockEppLib,
|
||||||
GenericTestHelper,
|
GenericTestHelper,
|
||||||
|
@ -48,7 +51,9 @@ class TestDomainAdminAsStaff(MockEppLib):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
self.superuser = create_superuser()
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
|
self.omb_analyst = create_omb_analyst_user()
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
@ -56,6 +61,24 @@ class TestDomainAdminAsStaff(MockEppLib):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.client.force_login(self.staffuser)
|
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()
|
super().setUp()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -65,12 +88,134 @@ class TestDomainAdminAsStaff(MockEppLib):
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
self.fed_agency.delete()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(self):
|
def tearDownClass(self):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
super().tearDownClass()
|
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
|
@less_console_noise_decorator
|
||||||
def test_staff_can_see_cisa_region_federal(self):
|
def test_staff_can_see_cisa_region_federal(self):
|
||||||
"""Tests if staff can see CISA Region: N/A"""
|
"""Tests if staff can see CISA Region: N/A"""
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from registrar.models.federal_agency import FederalAgency
|
||||||
|
from registrar.utility.constants import BranchChoices
|
||||||
from waffle.testutils import override_flag
|
from waffle.testutils import override_flag
|
||||||
import re
|
import re
|
||||||
from django.test import RequestFactory, Client, TestCase, override_settings
|
from django.test import RequestFactory, Client, TestCase, override_settings
|
||||||
|
@ -37,6 +39,7 @@ from .common import (
|
||||||
less_console_noise,
|
less_console_noise,
|
||||||
create_superuser,
|
create_superuser,
|
||||||
create_user,
|
create_user,
|
||||||
|
create_omb_analyst_user,
|
||||||
multiple_unalphabetical_domain_objects,
|
multiple_unalphabetical_domain_objects,
|
||||||
MockEppLib,
|
MockEppLib,
|
||||||
GenericTestHelper,
|
GenericTestHelper,
|
||||||
|
@ -68,6 +71,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site)
|
self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site)
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
|
self.ombanalyst = create_omb_analyst_user()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.test_helper = GenericTestHelper(
|
self.test_helper = GenericTestHelper(
|
||||||
factory=self.factory,
|
factory=self.factory,
|
||||||
|
@ -80,6 +84,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
allowed_emails = [AllowedEmail(email="mayor@igorville.gov"), AllowedEmail(email="help@get.gov")]
|
allowed_emails = [AllowedEmail(email="mayor@igorville.gov"), AllowedEmail(email="help@get.gov")]
|
||||||
AllowedEmail.objects.bulk_create(allowed_emails)
|
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):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
Host.objects.all().delete()
|
Host.objects.all().delete()
|
||||||
|
@ -92,6 +102,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
SeniorOfficial.objects.all().delete()
|
SeniorOfficial.objects.all().delete()
|
||||||
Suborganization.objects.all().delete()
|
Suborganization.objects.all().delete()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
|
self.fed_agency.delete()
|
||||||
self.mock_client.EMAILS_SENT.clear()
|
self.mock_client.EMAILS_SENT.clear()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -100,6 +111,71 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
AllowedEmail.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)
|
@override_flag("organization_feature", active=True)
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_clean_validates_duplicate_suborganization(self):
|
def test_clean_validates_duplicate_suborganization(self):
|
||||||
|
@ -2072,6 +2148,86 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
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):
|
def test_saving_when_restricted_creator(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
|
|
|
@ -13,7 +13,8 @@ from registrar.utility.email_invitations import (
|
||||||
_send_portfolio_admin_addition_emails_to_portfolio_admins,
|
_send_portfolio_admin_addition_emails_to_portfolio_admins,
|
||||||
_send_portfolio_admin_removal_emails_to_portfolio_admins,
|
_send_portfolio_admin_removal_emails_to_portfolio_admins,
|
||||||
send_domain_invitation_email,
|
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_addition_emails,
|
||||||
send_portfolio_admin_removal_emails,
|
send_portfolio_admin_removal_emails,
|
||||||
send_portfolio_invitation_email,
|
send_portfolio_invitation_email,
|
||||||
|
@ -33,7 +34,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||||
@patch("registrar.utility.email_invitations._validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
@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")
|
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||||
def test_send_domain_invitation_email(
|
def test_send_domain_invitation_email(
|
||||||
self,
|
self,
|
||||||
|
@ -98,7 +99,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||||
@patch("registrar.utility.email_invitations._validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
@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")
|
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||||
def test_send_domain_invitation_email_multiple_domains(
|
def test_send_domain_invitation_email_multiple_domains(
|
||||||
self,
|
self,
|
||||||
|
@ -234,7 +235,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@patch("registrar.utility.email_invitations._validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
@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")
|
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||||
def test_send_domain_invitation_email_raises_sending_email_exception(
|
def test_send_domain_invitation_email_raises_sending_email_exception(
|
||||||
self,
|
self,
|
||||||
|
@ -281,10 +282,10 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
self.assertEqual(str(context.exception), "Error sending email")
|
self.assertEqual(str(context.exception), "Error sending email")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@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._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations._get_requestor_email")
|
@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")
|
@patch("registrar.utility.email_invitations._normalize_domains")
|
||||||
def test_send_domain_invitation_email_manager_emails_send_mail_exception(
|
def test_send_domain_invitation_email_manager_emails_send_mail_exception(
|
||||||
self,
|
self,
|
||||||
|
@ -295,7 +296,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_send_domain_manager_emails,
|
mock_send_domain_manager_emails,
|
||||||
):
|
):
|
||||||
"""Test sending domain invitation email for one domain and assert exception
|
"""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
|
# Setup
|
||||||
mock_domain = MagicMock(name="domain1")
|
mock_domain = MagicMock(name="domain1")
|
||||||
|
@ -354,7 +355,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_send_templated_email.return_value = None # No exception means success
|
mock_send_templated_email.return_value = None # No exception means success
|
||||||
|
|
||||||
# Call function
|
# 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
|
# Assertions
|
||||||
self.assertTrue(result) # All emails should be successfully sent
|
self.assertTrue(result) # All emails should be successfully sent
|
||||||
|
@ -394,7 +395,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_send_templated_email.side_effect = EmailSendingError("Email sending failed")
|
mock_send_templated_email.side_effect = EmailSendingError("Email sending failed")
|
||||||
|
|
||||||
# Call function
|
# 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
|
# Assertions
|
||||||
self.assertFalse(result) # The result should be False as email sending failed
|
self.assertFalse(result) # The result should be False as email sending failed
|
||||||
|
@ -426,7 +427,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_filter.return_value = []
|
mock_filter.return_value = []
|
||||||
|
|
||||||
# Call function
|
# 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
|
# Assertions
|
||||||
self.assertTrue(result) # No emails to send, so it should return True
|
self.assertTrue(result) # No emails to send, so it should return True
|
||||||
|
@ -457,7 +458,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_send_templated_email.side_effect = [None, EmailSendingError("Failed to send email")]
|
mock_send_templated_email.side_effect = [None, EmailSendingError("Failed to send email")]
|
||||||
|
|
||||||
# Call function
|
# 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
|
# Assertions
|
||||||
self.assertFalse(result) # One email failed, so result should be False
|
self.assertFalse(result) # One email failed, so result should be False
|
||||||
|
@ -1112,3 +1113,151 @@ class TestSendPortfolioInvitationRemoveEmail(unittest.TestCase):
|
||||||
|
|
||||||
# Assertions
|
# Assertions
|
||||||
mock_logger.warning.assert_not_called() # Function should fail before logging email failure
|
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)
|
||||||
|
|
|
@ -1106,13 +1106,13 @@ class TestRegistrantContacts(MockEppLib):
|
||||||
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
|
domain, _ = Domain.objects.get_or_create(name="freeman.gov")
|
||||||
dummy_contact = domain.get_default_security_contact()
|
dummy_contact = domain.get_default_security_contact()
|
||||||
|
|
||||||
|
DF = common.DiscloseField
|
||||||
# Create contact with empty fields list
|
# Create contact with empty fields list
|
||||||
result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={})
|
result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={DF.EMAIL})
|
||||||
|
|
||||||
# Verify disclosure settings
|
# Verify disclosure settings
|
||||||
DF = common.DiscloseField
|
|
||||||
self.assertEqual(result.disclose.flag, True)
|
self.assertEqual(result.disclose.flag, True)
|
||||||
self.assertEqual(result.disclose.fields, {})
|
self.assertEqual(result.disclose.fields, {DF.EMAIL})
|
||||||
self.assertEqual(result.disclose.types, {DF.ADDR: "loc"})
|
self.assertEqual(result.disclose.types, {DF.ADDR: "loc"})
|
||||||
|
|
||||||
def test_not_disclosed_on_default_security_contact(self):
|
def test_not_disclosed_on_default_security_contact(self):
|
||||||
|
|
|
@ -72,7 +72,7 @@ class CsvReportsTest(MockDbForSharedTests):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
|
@ -94,7 +94,7 @@ class CsvReportsTest(MockDbForSharedTests):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||||
|
@ -261,9 +261,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
||||||
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
|
|
||||||
"World War I Centennial Commission,,,, ,,(blank),"
|
|
||||||
"meoward@rocks.com,\n"
|
|
||||||
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
|
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
|
||||||
"squeaker@rocks.com\n"
|
"squeaker@rocks.com\n"
|
||||||
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
@ -274,6 +271,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||||
|
"cdomain11.gov,Ready,2024-04-02,(blank),Federal,"
|
||||||
|
"World War I Centennial Commission,,,, ,,(blank),"
|
||||||
|
"meoward@rocks.com,\n"
|
||||||
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
|
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -498,7 +498,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||||
|
@ -538,7 +538,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||||
|
@ -594,7 +594,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"State,Status,Expiration date, Deleted\n"
|
"State,Status,Expiration date, Deleted\n"
|
||||||
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Portfolio1FederalAgency,Ready,(blank)\n"
|
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Portfolio1FederalAgency,Ready,(blank)\n"
|
||||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
||||||
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
"cdomain11.gov,Federal,WorldWarICentennialCommission,Ready,(blank)\n"
|
||||||
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
||||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
||||||
"sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
"sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||||
|
@ -642,7 +642,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"3,2,1,0,0,0,0,0,0,0\n"
|
"3,2,1,0,0,0,0,0,0,0\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Domain name,Domain type,Domain managers,Invited domain managers\n"
|
"Domain name,Domain type,Domain managers,Invited domain managers\n"
|
||||||
"cdomain11.gov,Federal - Executive,meoward@rocks.com,\n"
|
"cdomain11.gov,Federal,meoward@rocks.com,\n"
|
||||||
'cdomain1.gov,Federal - Executive,"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
|
'cdomain1.gov,Federal - Executive,"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
|
||||||
"woofwardthethird@rocks.com\n"
|
"woofwardthethird@rocks.com\n"
|
||||||
"zdomain12.gov,Interstate,meoward@rocks.com,\n"
|
"zdomain12.gov,Interstate,meoward@rocks.com,\n"
|
||||||
|
@ -716,7 +716,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain request,Domain type,Federal type\n"
|
"Domain request,Domain type,Federal type\n"
|
||||||
"city3.gov,Federal,Executive\n"
|
"city3.gov,Federal,Executive\n"
|
||||||
"city4.gov,City,Executive\n"
|
"city4.gov,City,\n"
|
||||||
"city6.gov,Federal,Executive\n"
|
"city6.gov,Federal,Executive\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -783,7 +783,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
|
"SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||||
"CISA regional representative,Current websites,Investigator\n"
|
"CISA regional representative,Current websites,Investigator\n"
|
||||||
# Content
|
# Content
|
||||||
"city5.gov,Approved,Federal,No,Executive,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
"city5.gov,Approved,Federal,No,,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
||||||
"city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more,"
|
"city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more,"
|
||||||
"Testy Tester testy2@town.com,,city.com,\n"
|
"Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,"
|
"city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,"
|
||||||
|
@ -795,7 +795,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
||||||
'Testy Tester testy2@town.com",'
|
'Testy Tester testy2@town.com",'
|
||||||
'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
||||||
"city4.gov,Submitted,City,No,Executive,,Testorg,Yes,,NY,2,,,,,,,,0,1,city1.gov,Testy,"
|
"city4.gov,Submitted,City,No,,,Testorg,Yes,,NY,2,,,,,,,,0,1,city1.gov,Testy,"
|
||||||
"Tester,testy@town.com,"
|
"Tester,testy@town.com,"
|
||||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
"Testy Tester testy2@town.com,"
|
"Testy Tester testy2@town.com,"
|
||||||
|
|
|
@ -1084,8 +1084,8 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@patch("registrar.views.domain.send_templated_email")
|
@patch("registrar.views.domain.send_domain_manager_removal_emails_to_domain_managers")
|
||||||
def test_domain_remove_manager(self, mock_send_templated_email):
|
def test_domain_remove_manager(self, mock_send_email):
|
||||||
"""Removing a domain manager sends notification email to other domain managers."""
|
"""Removing a domain manager sends notification email to other domain managers."""
|
||||||
self.manager, _ = User.objects.get_or_create(email="mayor@igorville.com", first_name="Hello", last_name="World")
|
self.manager, _ = User.objects.get_or_create(email="mayor@igorville.com", first_name="Hello", last_name="World")
|
||||||
self.manager_domain_permission, _ = UserDomainRole.objects.get_or_create(user=self.manager, domain=self.domain)
|
self.manager_domain_permission, _ = UserDomainRole.objects.get_or_create(user=self.manager, domain=self.domain)
|
||||||
|
@ -1094,11 +1094,11 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify that the notification emails were sent to domain manager
|
# Verify that the notification emails were sent to domain manager
|
||||||
mock_send_templated_email.assert_called_once_with(
|
mock_send_email.assert_called_once_with(
|
||||||
"emails/domain_manager_deleted_notification.txt",
|
removed_by_user=self.user,
|
||||||
"emails/domain_manager_deleted_notification_subject.txt",
|
manager_removed=self.manager,
|
||||||
to_address="info@example.com",
|
manager_removed_email=self.manager.email,
|
||||||
context=ANY,
|
domain=self.domain,
|
||||||
)
|
)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
|
|
@ -1657,16 +1657,19 @@ class TestPortfolioMemberDeleteView(WebTest):
|
||||||
self.user = create_test_user()
|
self.user = create_test_user()
|
||||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||||
|
self.domain_information, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user, domain=self.domain, portfolio=self.portfolio
|
||||||
|
)
|
||||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
UserPortfolioPermission.objects.all().delete()
|
UserPortfolioPermission.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
@ -1676,7 +1679,10 @@ class TestPortfolioMemberDeleteView(WebTest):
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||||
def test_portfolio_member_delete_view_members_table_active_requests(self, send_member_removal, send_removal_emails):
|
@patch("registrar.views.portfolios.send_domain_manager_removal_emails_to_domain_managers")
|
||||||
|
def test_portfolio_member_delete_view_members_table_active_requests(
|
||||||
|
self, send_domain_manager_removal_emails, send_member_removal, send_removal_emails
|
||||||
|
):
|
||||||
"""Error state w/ deleting a member with active request on Members Table"""
|
"""Error state w/ deleting a member with active request on Members Table"""
|
||||||
# I'm a user
|
# I'm a user
|
||||||
UserPortfolioPermission.objects.get_or_create(
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
@ -1718,13 +1724,18 @@ class TestPortfolioMemberDeleteView(WebTest):
|
||||||
send_removal_emails.assert_not_called()
|
send_removal_emails.assert_not_called()
|
||||||
# assert that send_portfolio_member_permission_remove_email is not called
|
# assert that send_portfolio_member_permission_remove_email is not called
|
||||||
send_member_removal.assert_not_called()
|
send_member_removal.assert_not_called()
|
||||||
|
# assert that send_domain_manager_removal_emails is not called
|
||||||
|
send_domain_manager_removal_emails.assert_not_called()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||||
def test_portfolio_member_delete_view_members_table_only_admin(self, send_member_removal, send_removal_emails):
|
@patch("registrar.views.portfolios.send_domain_manager_removal_emails_to_domain_managers")
|
||||||
|
def test_portfolio_member_delete_view_members_table_only_admin(
|
||||||
|
self, send_domain_manager_removal_emails, send_member_removal, send_removal_emails
|
||||||
|
):
|
||||||
"""Error state w/ deleting a member that's the only admin on Members Table"""
|
"""Error state w/ deleting a member that's the only admin on Members Table"""
|
||||||
|
|
||||||
# I'm a user with admin permission
|
# I'm a user with admin permission
|
||||||
|
@ -1757,13 +1768,18 @@ class TestPortfolioMemberDeleteView(WebTest):
|
||||||
send_removal_emails.assert_not_called()
|
send_removal_emails.assert_not_called()
|
||||||
# assert that send_portfolio_member_permission_remove_email is not called
|
# assert that send_portfolio_member_permission_remove_email is not called
|
||||||
send_member_removal.assert_not_called()
|
send_member_removal.assert_not_called()
|
||||||
|
# assert that send_domain_manager_removal_emails is not called
|
||||||
|
send_domain_manager_removal_emails.assert_not_called()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
@patch("registrar.views.portfolios.send_portfolio_admin_removal_emails")
|
||||||
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
@patch("registrar.views.portfolios.send_portfolio_member_permission_remove_email")
|
||||||
def test_portfolio_member_table_delete_member_success(self, send_member_removal, mock_send_removal_emails):
|
@patch("registrar.views.portfolios.send_domain_manager_removal_emails_to_domain_managers")
|
||||||
|
def test_portfolio_member_table_delete_member_success(
|
||||||
|
self, send_domain_manager_removal_emails, send_member_removal, mock_send_removal_emails
|
||||||
|
):
|
||||||
"""Success state with deleting on Members Table page bc no active request AND not only admin"""
|
"""Success state with deleting on Members Table page bc no active request AND not only admin"""
|
||||||
|
|
||||||
# I'm a user
|
# I'm a user
|
||||||
|
@ -1788,6 +1804,9 @@ class TestPortfolioMemberDeleteView(WebTest):
|
||||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set up the member as a domain manager
|
||||||
|
UserDomainRole.objects.get_or_create(user=member, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
# Member removal email sent successfully
|
# Member removal email sent successfully
|
||||||
send_member_removal.return_value = True
|
send_member_removal.return_value = True
|
||||||
|
|
||||||
|
@ -1815,6 +1834,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
||||||
mock_send_removal_emails.assert_not_called()
|
mock_send_removal_emails.assert_not_called()
|
||||||
# assert that send_portfolio_member_permission_remove_email is called
|
# assert that send_portfolio_member_permission_remove_email is called
|
||||||
send_member_removal.assert_called_once()
|
send_member_removal.assert_called_once()
|
||||||
|
# assert that send_domain_manager_removal_emails_to_domain_managers
|
||||||
|
send_domain_manager_removal_emails.assert_called_once()
|
||||||
|
|
||||||
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
# Get the arguments passed to send_portfolio_member_permission_remove_email
|
||||||
_, called_kwargs = send_member_removal.call_args
|
_, called_kwargs = send_member_removal.call_args
|
||||||
|
@ -1824,6 +1845,15 @@ class TestPortfolioMemberDeleteView(WebTest):
|
||||||
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
self.assertEqual(called_kwargs["permissions"].user, upp.user)
|
||||||
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
self.assertEqual(called_kwargs["permissions"].portfolio, upp.portfolio)
|
||||||
|
|
||||||
|
# Get the arguments passed to send_domain_manager_removal_emails_to_domain_managers
|
||||||
|
_, called_kwargs = send_domain_manager_removal_emails.call_args
|
||||||
|
|
||||||
|
# Assert the email content
|
||||||
|
self.assertEqual(called_kwargs["removed_by_user"], self.user)
|
||||||
|
self.assertEqual(called_kwargs["manager_removed"], upp.user)
|
||||||
|
self.assertEqual(called_kwargs["manager_removed_email"], upp.user.email)
|
||||||
|
self.assertEqual(called_kwargs["domain"], self.domain)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
|
@ -2639,7 +2669,8 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
@patch("registrar.views.portfolios.send_domain_invitation_email")
|
@patch("registrar.views.portfolios.send_domain_invitation_email")
|
||||||
def test_post_with_valid_added_domains(self, mock_send_domain_email):
|
@patch("registrar.views.portfolios.send_domain_manager_removal_emails_to_domain_managers")
|
||||||
|
def test_post_with_valid_added_domains(self, send_domain_manager_removal_emails, mock_send_domain_email):
|
||||||
"""Test that domains can be successfully added."""
|
"""Test that domains can be successfully added."""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
@ -2658,6 +2689,8 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
||||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||||
|
|
||||||
expected_domains = [self.domain1, self.domain2, self.domain3]
|
expected_domains = [self.domain1, self.domain2, self.domain3]
|
||||||
|
# assert that send_domain_manager_removal_emails_to_domain_managers is not called
|
||||||
|
send_domain_manager_removal_emails.assert_not_called()
|
||||||
# Verify that the invitation email was sent
|
# Verify that the invitation email was sent
|
||||||
mock_send_domain_email.assert_called_once()
|
mock_send_domain_email.assert_called_once()
|
||||||
call_args = mock_send_domain_email.call_args.kwargs
|
call_args = mock_send_domain_email.call_args.kwargs
|
||||||
|
@ -2670,7 +2703,8 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
@patch("registrar.views.portfolios.send_domain_invitation_email")
|
@patch("registrar.views.portfolios.send_domain_invitation_email")
|
||||||
def test_post_with_valid_removed_domains(self, mock_send_domain_email):
|
@patch("registrar.views.portfolios.send_domain_manager_removal_emails_to_domain_managers")
|
||||||
|
def test_post_with_valid_removed_domains(self, send_domain_manager_removal_emails, mock_send_domain_email):
|
||||||
"""Test that domains can be successfully removed."""
|
"""Test that domains can be successfully removed."""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
@ -2678,6 +2712,8 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
||||||
domains = [self.domain1, self.domain2, self.domain3]
|
domains = [self.domain1, self.domain2, self.domain3]
|
||||||
UserDomainRole.objects.bulk_create([UserDomainRole(domain=domain, user=self.user) for domain in domains])
|
UserDomainRole.objects.bulk_create([UserDomainRole(domain=domain, user=self.user) for domain in domains])
|
||||||
|
|
||||||
|
send_domain_manager_removal_emails.return_value = True
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"removed_domains": json.dumps([self.domain1.id, self.domain2.id]),
|
"removed_domains": json.dumps([self.domain1.id, self.domain2.id]),
|
||||||
}
|
}
|
||||||
|
@ -2694,7 +2730,19 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
||||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||||
# assert that send_domain_invitation_email is not called
|
# assert that send_domain_invitation_email is not called
|
||||||
mock_send_domain_email.assert_not_called()
|
mock_send_domain_email.assert_not_called()
|
||||||
|
# assert that send_domain_manager_removal_emails_to_domain_managers is called twice
|
||||||
|
send_domain_manager_removal_emails.assert_any_call(
|
||||||
|
removed_by_user=self.user,
|
||||||
|
manager_removed=self.portfolio_permission.user,
|
||||||
|
manager_removed_email=self.portfolio_permission.user.email,
|
||||||
|
domain=self.domain1,
|
||||||
|
)
|
||||||
|
send_domain_manager_removal_emails.assert_any_call(
|
||||||
|
removed_by_user=self.user,
|
||||||
|
manager_removed=self.portfolio_permission.user,
|
||||||
|
manager_removed_email=self.portfolio_permission.user.email,
|
||||||
|
domain=self.domain2,
|
||||||
|
)
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
|
|
@ -579,8 +579,8 @@ class DomainExport(BaseExport):
|
||||||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
then=F("portfolio__federal_agency__federal_type"),
|
then=F("portfolio__federal_agency__federal_type"),
|
||||||
),
|
),
|
||||||
# Otherwise, return the natively assigned value
|
# Otherwise, return the federal type from federal agency
|
||||||
default=F("federal_type"),
|
default=F("federal_agency__federal_type"),
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
),
|
),
|
||||||
"converted_organization_name": Case(
|
"converted_organization_name": Case(
|
||||||
|
@ -740,7 +740,7 @@ class DomainExport(BaseExport):
|
||||||
domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
|
domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
|
||||||
|
|
||||||
security_contact_email = model.get("security_contact_email")
|
security_contact_email = model.get("security_contact_email")
|
||||||
invalid_emails = [email for email in DefaultEmail]
|
invalid_emails = DefaultEmail.get_all_emails()
|
||||||
if (
|
if (
|
||||||
not security_contact_email
|
not security_contact_email
|
||||||
or not isinstance(security_contact_email, str)
|
or not isinstance(security_contact_email, str)
|
||||||
|
@ -1654,8 +1654,8 @@ class DomainRequestExport(BaseExport):
|
||||||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||||
then=F("portfolio__federal_agency__federal_type"),
|
then=F("portfolio__federal_agency__federal_type"),
|
||||||
),
|
),
|
||||||
# Otherwise, return the natively assigned value
|
# Otherwise, return the federal type from federal agency
|
||||||
default=F("federal_type"),
|
default=F("federal_agency__federal_type"),
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
),
|
),
|
||||||
"converted_organization_name": Case(
|
"converted_organization_name": Case(
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.conf import settings
|
||||||
from registrar.models import Domain, DomainInvitation, UserDomainRole
|
from registrar.models import Domain, DomainInvitation, UserDomainRole
|
||||||
from registrar.models.portfolio import Portfolio
|
from registrar.models.portfolio import Portfolio
|
||||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
|
from registrar.models.user import User
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
from registrar.utility.errors import (
|
from registrar.utility.errors import (
|
||||||
|
@ -18,6 +19,88 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_domains(domains: Domain | list[Domain]) -> list[Domain]:
|
||||||
|
"""Ensures domains is always a list."""
|
||||||
|
return [domains] if isinstance(domains, Domain) else domains
|
||||||
|
|
||||||
|
|
||||||
|
def _get_requestor_email(requestor, domains=None, portfolio=None):
|
||||||
|
"""Get the requestor's email or raise an error if it's missing.
|
||||||
|
|
||||||
|
If the requestor is staff, default email is returned.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
MissingEmailError
|
||||||
|
"""
|
||||||
|
if requestor.is_staff:
|
||||||
|
return settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
if not requestor.email or requestor.email.strip() == "":
|
||||||
|
domain_names = None
|
||||||
|
if domains:
|
||||||
|
domain_names = ", ".join([domain.name for domain in domains])
|
||||||
|
raise MissingEmailError(email=requestor.email, domain=domain_names, portfolio=portfolio)
|
||||||
|
|
||||||
|
return requestor.email
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_invitation(email, user, domains, requestor, is_member_of_different_org):
|
||||||
|
"""Validate the invitation conditions."""
|
||||||
|
_check_outside_org_membership(email, requestor, is_member_of_different_org)
|
||||||
|
|
||||||
|
for domain in domains:
|
||||||
|
_validate_existing_invitation(email, user, domain)
|
||||||
|
|
||||||
|
# NOTE: should we also be validating against existing user_domain_roles
|
||||||
|
|
||||||
|
|
||||||
|
def _check_outside_org_membership(email, requestor, is_member_of_different_org):
|
||||||
|
"""Raise an error if the email belongs to a different organization."""
|
||||||
|
if (
|
||||||
|
flag_is_active_for_user(requestor, "organization_feature")
|
||||||
|
and not flag_is_active_for_user(requestor, "multiple_portfolios")
|
||||||
|
and is_member_of_different_org
|
||||||
|
):
|
||||||
|
raise OutsideOrgMemberError(email=email)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_existing_invitation(email, user, domain):
|
||||||
|
"""Check for existing invitations and handle their status."""
|
||||||
|
try:
|
||||||
|
invite = DomainInvitation.objects.get(email=email, domain=domain)
|
||||||
|
if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
|
||||||
|
raise AlreadyDomainManagerError(email)
|
||||||
|
elif invite.status == DomainInvitation.DomainInvitationStatus.CANCELED:
|
||||||
|
invite.update_cancellation_status()
|
||||||
|
invite.save()
|
||||||
|
else:
|
||||||
|
raise AlreadyDomainInvitedError(email)
|
||||||
|
except DomainInvitation.DoesNotExist:
|
||||||
|
pass
|
||||||
|
if user:
|
||||||
|
if UserDomainRole.objects.filter(user=user, domain=domain).exists():
|
||||||
|
raise AlreadyDomainManagerError(email)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_domain_invitation_email(email, requestor_email, domains, requested_user):
|
||||||
|
"""Send the invitation email."""
|
||||||
|
try:
|
||||||
|
send_templated_email(
|
||||||
|
"emails/domain_invitation.txt",
|
||||||
|
"emails/domain_invitation_subject.txt",
|
||||||
|
to_address=email,
|
||||||
|
context={
|
||||||
|
"domains": domains,
|
||||||
|
"requestor_email": requestor_email,
|
||||||
|
"invitee_email_address": email,
|
||||||
|
"requested_user": requested_user,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except EmailSendingError as err:
|
||||||
|
domain_names = ", ".join([domain.name for domain in domains])
|
||||||
|
raise EmailSendingError(f"Could not send email invitation to {email} for domains: {domain_names}") from err
|
||||||
|
|
||||||
|
|
||||||
def send_domain_invitation_email(
|
def send_domain_invitation_email(
|
||||||
email: str, requestor, domains: Domain | list[Domain], is_member_of_different_org, requested_user=None
|
email: str, requestor, domains: Domain | list[Domain], is_member_of_different_org, requested_user=None
|
||||||
):
|
):
|
||||||
|
@ -46,12 +129,12 @@ def send_domain_invitation_email(
|
||||||
|
|
||||||
_validate_invitation(email, requested_user, domains, requestor, is_member_of_different_org)
|
_validate_invitation(email, requested_user, domains, requestor, is_member_of_different_org)
|
||||||
|
|
||||||
send_invitation_email(email, requestor_email, domains, requested_user)
|
_send_domain_invitation_email(email, requestor_email, domains, requested_user)
|
||||||
|
|
||||||
all_manager_emails_sent = True
|
all_manager_emails_sent = True
|
||||||
# send emails to domain managers
|
# send emails to domain managers
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
if not send_emails_to_domain_managers(
|
if not _send_domain_invitation_update_emails_to_domain_managers(
|
||||||
email=email,
|
email=email,
|
||||||
requestor_email=requestor_email,
|
requestor_email=requestor_email,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
|
@ -62,7 +145,9 @@ def send_domain_invitation_email(
|
||||||
return all_manager_emails_sent
|
return all_manager_emails_sent
|
||||||
|
|
||||||
|
|
||||||
def send_emails_to_domain_managers(email: str, requestor_email, domain: Domain, requested_user=None):
|
def _send_domain_invitation_update_emails_to_domain_managers(
|
||||||
|
email: str, requestor_email, domain: Domain, requested_user=None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Notifies all domain managers of the provided domain of a change
|
Notifies all domain managers of the provided domain of a change
|
||||||
|
|
||||||
|
@ -96,86 +181,54 @@ def send_emails_to_domain_managers(email: str, requestor_email, domain: Domain,
|
||||||
return all_emails_sent
|
return all_emails_sent
|
||||||
|
|
||||||
|
|
||||||
def _normalize_domains(domains: Domain | list[Domain]) -> list[Domain]:
|
def send_domain_manager_removal_emails_to_domain_managers(
|
||||||
"""Ensures domains is always a list."""
|
removed_by_user: User,
|
||||||
return [domains] if isinstance(domains, Domain) else domains
|
manager_removed: User,
|
||||||
|
manager_removed_email: str,
|
||||||
|
domain: Domain,
|
||||||
def _get_requestor_email(requestor, domains=None, portfolio=None):
|
):
|
||||||
"""Get the requestor's email or raise an error if it's missing.
|
|
||||||
|
|
||||||
If the requestor is staff, default email is returned.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
MissingEmailError
|
|
||||||
"""
|
"""
|
||||||
if requestor.is_staff:
|
Notifies all domain managers that a domain manager has been removed.
|
||||||
return settings.DEFAULT_FROM_EMAIL
|
|
||||||
|
|
||||||
if not requestor.email or requestor.email.strip() == "":
|
Args:
|
||||||
domain_names = None
|
removed_by_user(User): The user who initiated the removal.
|
||||||
if domains:
|
manager_removed(User): The user being removed.
|
||||||
domain_names = ", ".join([domain.name for domain in domains])
|
manager_removed_email(str): The email of the user being removed (in case no User).
|
||||||
raise MissingEmailError(email=requestor.email, domain=domain_names, portfolio=portfolio)
|
domain(Domain): The domain the user is being removed from.
|
||||||
|
|
||||||
return requestor.email
|
Returns:
|
||||||
|
Boolean indicating if all messages were sent successfully.
|
||||||
|
|
||||||
|
"""
|
||||||
def _validate_invitation(email, user, domains, requestor, is_member_of_different_org):
|
all_emails_sent = True
|
||||||
"""Validate the invitation conditions."""
|
# Get each domain manager from list
|
||||||
check_outside_org_membership(email, requestor, is_member_of_different_org)
|
user_domain_roles = UserDomainRole.objects.filter(domain=domain)
|
||||||
|
if manager_removed:
|
||||||
for domain in domains:
|
user_domain_roles = user_domain_roles.exclude(user=manager_removed)
|
||||||
_validate_existing_invitation(email, user, domain)
|
for user_domain_role in user_domain_roles:
|
||||||
|
# Send email to each domain manager
|
||||||
# NOTE: should we also be validating against existing user_domain_roles
|
user = user_domain_role.user
|
||||||
|
|
||||||
|
|
||||||
def check_outside_org_membership(email, requestor, is_member_of_different_org):
|
|
||||||
"""Raise an error if the email belongs to a different organization."""
|
|
||||||
if (
|
|
||||||
flag_is_active_for_user(requestor, "organization_feature")
|
|
||||||
and not flag_is_active_for_user(requestor, "multiple_portfolios")
|
|
||||||
and is_member_of_different_org
|
|
||||||
):
|
|
||||||
raise OutsideOrgMemberError(email=email)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_existing_invitation(email, user, domain):
|
|
||||||
"""Check for existing invitations and handle their status."""
|
|
||||||
try:
|
|
||||||
invite = DomainInvitation.objects.get(email=email, domain=domain)
|
|
||||||
if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
|
|
||||||
raise AlreadyDomainManagerError(email)
|
|
||||||
elif invite.status == DomainInvitation.DomainInvitationStatus.CANCELED:
|
|
||||||
invite.update_cancellation_status()
|
|
||||||
invite.save()
|
|
||||||
else:
|
|
||||||
raise AlreadyDomainInvitedError(email)
|
|
||||||
except DomainInvitation.DoesNotExist:
|
|
||||||
pass
|
|
||||||
if user:
|
|
||||||
if UserDomainRole.objects.filter(user=user, domain=domain).exists():
|
|
||||||
raise AlreadyDomainManagerError(email)
|
|
||||||
|
|
||||||
|
|
||||||
def send_invitation_email(email, requestor_email, domains, requested_user):
|
|
||||||
"""Send the invitation email."""
|
|
||||||
try:
|
try:
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
"emails/domain_invitation.txt",
|
"emails/domain_manager_deleted_notification.txt",
|
||||||
"emails/domain_invitation_subject.txt",
|
"emails/domain_manager_deleted_notification_subject.txt",
|
||||||
to_address=email,
|
to_address=user.email,
|
||||||
context={
|
context={
|
||||||
"domains": domains,
|
"domain": domain,
|
||||||
"requestor_email": requestor_email,
|
"removed_by": removed_by_user,
|
||||||
"invitee_email_address": email,
|
"manager_removed_email": manager_removed_email,
|
||||||
"requested_user": requested_user,
|
"date": date.today(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except EmailSendingError as err:
|
except EmailSendingError:
|
||||||
domain_names = ", ".join([domain.name for domain in domains])
|
logger.warning(
|
||||||
raise EmailSendingError(f"Could not send email invitation to {email} for domains: {domain_names}") from err
|
"Could not send notification email to %s for domain %s",
|
||||||
|
user.email,
|
||||||
|
domain.name,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
all_emails_sent = False
|
||||||
|
return all_emails_sent
|
||||||
|
|
||||||
|
|
||||||
def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_invitation):
|
def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_invitation):
|
||||||
|
|
|
@ -45,6 +45,10 @@ class DefaultEmail(StrEnum):
|
||||||
OLD_PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov"
|
OLD_PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov"
|
||||||
LEGACY_DEFAULT = "registrar@dotgov.gov"
|
LEGACY_DEFAULT = "registrar@dotgov.gov"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_emails(cls):
|
||||||
|
return [email for email in cls]
|
||||||
|
|
||||||
|
|
||||||
class DefaultUserValues(StrEnum):
|
class DefaultUserValues(StrEnum):
|
||||||
"""Stores default values for a default user.
|
"""Stores default values for a default user.
|
||||||
|
|
|
@ -68,7 +68,11 @@ from epplibwrapper import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
from ..utility.email_invitations import send_domain_invitation_email, send_portfolio_invitation_email
|
from ..utility.email_invitations import (
|
||||||
|
send_domain_invitation_email,
|
||||||
|
send_domain_manager_removal_emails_to_domain_managers,
|
||||||
|
send_portfolio_invitation_email,
|
||||||
|
)
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -398,7 +402,7 @@ class DomainView(DomainBaseView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
default_emails = [email for email in DefaultEmail]
|
default_emails = DefaultEmail.get_all_emails()
|
||||||
|
|
||||||
context["hidden_security_emails"] = default_emails
|
context["hidden_security_emails"] = default_emails
|
||||||
|
|
||||||
|
@ -456,7 +460,7 @@ class DomainRenewalView(DomainBaseView):
|
||||||
|
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
default_emails = [email for email in DefaultEmail]
|
default_emails = DefaultEmail.get_all_emails()
|
||||||
|
|
||||||
context["hidden_security_emails"] = default_emails
|
context["hidden_security_emails"] = default_emails
|
||||||
|
|
||||||
|
@ -1166,7 +1170,7 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
security_contact = self.object.security_contact
|
security_contact = self.object.security_contact
|
||||||
|
|
||||||
invalid_emails = [email for email in DefaultEmail]
|
invalid_emails = DefaultEmail.get_all_emails()
|
||||||
if security_contact is None or security_contact.email in invalid_emails:
|
if security_contact is None or security_contact.email in invalid_emails:
|
||||||
initial["security_email"] = None
|
initial["security_email"] = None
|
||||||
return initial
|
return initial
|
||||||
|
@ -1474,48 +1478,17 @@ class DomainDeleteUserView(DeleteView):
|
||||||
super().form_valid(form)
|
super().form_valid(form)
|
||||||
|
|
||||||
# Email all domain managers that domain manager has been removed
|
# Email all domain managers that domain manager has been removed
|
||||||
domain = self.object.domain
|
send_domain_manager_removal_emails_to_domain_managers(
|
||||||
|
removed_by_user=self.request.user,
|
||||||
context = {
|
manager_removed=self.object.user,
|
||||||
"domain": domain,
|
manager_removed_email=self.object.user.email,
|
||||||
"removed_by": self.request.user,
|
domain=self.object.domain,
|
||||||
"manager_removed": self.object.user,
|
|
||||||
"date": date.today(),
|
|
||||||
"changes": "Domain Manager",
|
|
||||||
}
|
|
||||||
self.email_domain_managers(
|
|
||||||
domain,
|
|
||||||
"emails/domain_manager_deleted_notification.txt",
|
|
||||||
"emails/domain_manager_deleted_notification_subject.txt",
|
|
||||||
context,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add a success message
|
# Add a success message
|
||||||
messages.success(self.request, self.get_success_message())
|
messages.success(self.request, self.get_success_message())
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def email_domain_managers(self, domain: Domain, template: str, subject_template: str, context={}):
|
|
||||||
manager_pks = UserDomainRole.objects.filter(domain=domain.pk, role=UserDomainRole.Roles.MANAGER).values_list(
|
|
||||||
"user", flat=True
|
|
||||||
)
|
|
||||||
emails = list(User.objects.filter(pk__in=manager_pks).values_list("email", flat=True))
|
|
||||||
|
|
||||||
for email in emails:
|
|
||||||
try:
|
|
||||||
send_templated_email(
|
|
||||||
template,
|
|
||||||
subject_template,
|
|
||||||
to_address=email,
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
except EmailSendingError:
|
|
||||||
logger.warning(
|
|
||||||
"Could not send notification email to %s for domain %s",
|
|
||||||
email,
|
|
||||||
domain.name,
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Custom post implementation to ensure last userdomainrole is not removed and to
|
"""Custom post implementation to ensure last userdomainrole is not removed and to
|
||||||
redirect to home in the event that the user deletes themselves"""
|
redirect to home in the event that the user deletes themselves"""
|
||||||
|
|
|
@ -29,6 +29,7 @@ from registrar.models.utility.portfolio_helper import UserPortfolioPermissionCho
|
||||||
from registrar.utility.email import EmailSendingError
|
from registrar.utility.email import EmailSendingError
|
||||||
from registrar.utility.email_invitations import (
|
from registrar.utility.email_invitations import (
|
||||||
send_domain_invitation_email,
|
send_domain_invitation_email,
|
||||||
|
send_domain_manager_removal_emails_to_domain_managers,
|
||||||
send_portfolio_admin_addition_emails,
|
send_portfolio_admin_addition_emails,
|
||||||
send_portfolio_admin_removal_emails,
|
send_portfolio_admin_removal_emails,
|
||||||
send_portfolio_invitation_email,
|
send_portfolio_invitation_email,
|
||||||
|
@ -193,6 +194,31 @@ class PortfolioMemberDeleteView(View):
|
||||||
messages.warning(
|
messages.warning(
|
||||||
request, f"Could not send email notification to {portfolio_member_permission.user.email}"
|
request, f"Could not send email notification to {portfolio_member_permission.user.email}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Notify domain managers for domains which the member is being removed from
|
||||||
|
# Get list of portfolio domains that the member is invited to:
|
||||||
|
invited_domains = Domain.objects.filter(
|
||||||
|
invitations__email=portfolio_member_permission.user.email,
|
||||||
|
domain_info__portfolio=portfolio_member_permission.portfolio,
|
||||||
|
invitations__status=DomainInvitation.DomainInvitationStatus.INVITED,
|
||||||
|
).distinct()
|
||||||
|
# Get list of portfolio domains that the member is a manager of
|
||||||
|
domains = Domain.objects.filter(
|
||||||
|
permissions__user=portfolio_member_permission.user,
|
||||||
|
domain_info__portfolio=portfolio_member_permission.portfolio,
|
||||||
|
).distinct()
|
||||||
|
# Combine both querysets while ensuring uniqueness
|
||||||
|
all_domains = domains.union(invited_domains)
|
||||||
|
for domain in all_domains:
|
||||||
|
if not send_domain_manager_removal_emails_to_domain_managers(
|
||||||
|
removed_by_user=request.user,
|
||||||
|
manager_removed=portfolio_member_permission.user,
|
||||||
|
manager_removed_email=portfolio_member_permission.user.email,
|
||||||
|
domain=domain,
|
||||||
|
):
|
||||||
|
messages.warning(
|
||||||
|
request, "Could not send email notification to existing domain managers for %s", domain
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handle_exceptions(e)
|
self._handle_exceptions(e)
|
||||||
|
|
||||||
|
@ -432,6 +458,20 @@ class PortfolioMemberDomainsEditView(DetailView, View):
|
||||||
Processes removed domains by deleting corresponding UserDomainRole instances.
|
Processes removed domains by deleting corresponding UserDomainRole instances.
|
||||||
"""
|
"""
|
||||||
if removed_domain_ids:
|
if removed_domain_ids:
|
||||||
|
# Notify domain managers for domains which the member is being removed from
|
||||||
|
# Fetch Domain objects from removed_domain_ids
|
||||||
|
removed_domains = Domain.objects.filter(id__in=removed_domain_ids)
|
||||||
|
# need to get the domains from removed_domain_ids
|
||||||
|
for domain in removed_domains:
|
||||||
|
if not send_domain_manager_removal_emails_to_domain_managers(
|
||||||
|
removed_by_user=self.request.user,
|
||||||
|
manager_removed=member,
|
||||||
|
manager_removed_email=member.email,
|
||||||
|
domain=domain,
|
||||||
|
):
|
||||||
|
messages.warning(
|
||||||
|
self.request, "Could not send email notification to existing domain managers for %s", domain
|
||||||
|
)
|
||||||
# Delete UserDomainRole instances for removed domains
|
# Delete UserDomainRole instances for removed domains
|
||||||
UserDomainRole.objects.filter(domain_id__in=removed_domain_ids, user=member).delete()
|
UserDomainRole.objects.filter(domain_id__in=removed_domain_ids, user=member).delete()
|
||||||
|
|
||||||
|
@ -502,6 +542,31 @@ class PortfolioInvitedMemberDeleteView(View):
|
||||||
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
messages.warning(self.request, "Could not send email notification to existing organization admins.")
|
||||||
if not send_portfolio_invitation_remove_email(requestor=request.user, invitation=portfolio_invitation):
|
if not send_portfolio_invitation_remove_email(requestor=request.user, invitation=portfolio_invitation):
|
||||||
messages.warning(request, f"Could not send email notification to {portfolio_invitation.email}")
|
messages.warning(request, f"Could not send email notification to {portfolio_invitation.email}")
|
||||||
|
|
||||||
|
# Notify domain managers for domains which the invited member is being removed from
|
||||||
|
# Get list of portfolio domains that the invited member is invited to:
|
||||||
|
invited_domains = Domain.objects.filter(
|
||||||
|
invitations__email=portfolio_invitation.email,
|
||||||
|
domain_info__portfolio=portfolio_invitation.portfolio,
|
||||||
|
invitations__status=DomainInvitation.DomainInvitationStatus.INVITED,
|
||||||
|
).distinct()
|
||||||
|
# Get list of portfolio domains that the member is a manager of
|
||||||
|
domains = Domain.objects.filter(
|
||||||
|
permissions__user__email=portfolio_invitation.email,
|
||||||
|
domain_info__portfolio=portfolio_invitation.portfolio,
|
||||||
|
).distinct()
|
||||||
|
# Combine both querysets while ensuring uniqueness
|
||||||
|
all_domains = domains.union(invited_domains)
|
||||||
|
for domain in all_domains:
|
||||||
|
if not send_domain_manager_removal_emails_to_domain_managers(
|
||||||
|
removed_by_user=request.user,
|
||||||
|
manager_removed=None,
|
||||||
|
manager_removed_email=portfolio_invitation.email,
|
||||||
|
domain=domain,
|
||||||
|
):
|
||||||
|
messages.warning(
|
||||||
|
request, "Could not send email notification to existing domain managers for %s", domain
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handle_exceptions(e)
|
self._handle_exceptions(e)
|
||||||
|
|
||||||
|
@ -740,6 +805,21 @@ class PortfolioInvitedMemberDomainsEditView(DetailView, View):
|
||||||
if not removed_domain_ids:
|
if not removed_domain_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Notify domain managers for domains which the member is being removed from
|
||||||
|
# Fetch Domain objects from removed_domain_ids
|
||||||
|
removed_domains = Domain.objects.filter(id__in=removed_domain_ids)
|
||||||
|
# need to get the domains from removed_domain_ids
|
||||||
|
for domain in removed_domains:
|
||||||
|
if not send_domain_manager_removal_emails_to_domain_managers(
|
||||||
|
removed_by_user=self.request.user,
|
||||||
|
manager_removed=None,
|
||||||
|
manager_removed_email=email,
|
||||||
|
domain=domain,
|
||||||
|
):
|
||||||
|
messages.warning(
|
||||||
|
self.request, "Could not send email notification to existing domain managers for %s", domain
|
||||||
|
)
|
||||||
|
|
||||||
# Update invitations from INVITED to CANCELED
|
# Update invitations from INVITED to CANCELED
|
||||||
DomainInvitation.objects.filter(
|
DomainInvitation.objects.filter(
|
||||||
domain_id__in=removed_domain_ids,
|
domain_id__in=removed_domain_ids,
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.shortcuts import render
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models import Avg, F
|
from django.db.models import Avg, F
|
||||||
|
|
||||||
from registrar.decorators import ALL, HAS_PORTFOLIO_MEMBERS_VIEW, IS_STAFF, grant_access
|
from registrar.decorators import ALL, HAS_PORTFOLIO_MEMBERS_VIEW, IS_CISA_ANALYST, IS_FULL_ACCESS, grant_access
|
||||||
from .. import models
|
from .. import models
|
||||||
import datetime
|
import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -16,7 +16,7 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class AnalyticsView(View):
|
class AnalyticsView(View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||||
|
@ -176,7 +176,7 @@ class AnalyticsView(View):
|
||||||
return render(request, "admin/analytics.html", context)
|
return render(request, "admin/analytics.html", context)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDataType(View):
|
class ExportDataType(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# match the CSV example with all the fields
|
# match the CSV example with all the fields
|
||||||
|
@ -227,7 +227,7 @@ class ExportMembersPortfolio(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDataFull(View):
|
class ExportDataFull(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Smaller export based on 1
|
# Smaller export based on 1
|
||||||
|
@ -237,7 +237,7 @@ class ExportDataFull(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDataFederal(View):
|
class ExportDataFederal(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Federal only
|
# Federal only
|
||||||
|
@ -247,7 +247,7 @@ class ExportDataFederal(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDomainRequestDataFull(View):
|
class ExportDomainRequestDataFull(View):
|
||||||
"""Generates a downloaded report containing all Domain Requests (except started)"""
|
"""Generates a downloaded report containing all Domain Requests (except started)"""
|
||||||
|
|
||||||
|
@ -259,7 +259,7 @@ class ExportDomainRequestDataFull(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDataDomainsGrowth(View):
|
class ExportDataDomainsGrowth(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
|
@ -272,7 +272,7 @@ class ExportDataDomainsGrowth(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDataRequestsGrowth(View):
|
class ExportDataRequestsGrowth(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
|
@ -285,7 +285,7 @@ class ExportDataRequestsGrowth(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDataManagedDomains(View):
|
class ExportDataManagedDomains(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
|
@ -297,7 +297,7 @@ class ExportDataManagedDomains(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class ExportDataUnmanagedDomains(View):
|
class ExportDataUnmanagedDomains(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.db.models import ForeignKey, OneToOneField, ManyToManyField, ManyToO
|
||||||
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from registrar.decorators import IS_STAFF, grant_access
|
from registrar.decorators import IS_CISA_ANALYST, IS_FULL_ACCESS, grant_access
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.domain_request import DomainRequest
|
from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
|
@ -19,7 +19,7 @@ from registrar.utility.db_helpers import ignore_unique_violation
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||||
class TransferUserView(View):
|
class TransferUserView(View):
|
||||||
"""Transfer user methods that set up the transfer_user template and handle the forms on it."""
|
"""Transfer user methods that set up the transfer_user template and handle the forms on it."""
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
from registrar.decorators import IS_STAFF, grant_access
|
from registrar.decorators import IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST, grant_access
|
||||||
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
||||||
from registrar.utility.admin_helpers import get_action_needed_reason_default_email, get_rejection_reason_default_email
|
from registrar.utility.admin_helpers import get_action_needed_reason_default_email, get_rejection_reason_default_email
|
||||||
from registrar.models.portfolio import Portfolio
|
from registrar.models.portfolio import Portfolio
|
||||||
|
@ -10,16 +10,10 @@ from registrar.utility.constants import BranchChoices
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||||
def get_senior_official_from_federal_agency_json(request):
|
def get_senior_official_from_federal_agency_json(request):
|
||||||
"""Returns federal_agency information as a JSON"""
|
"""Returns federal_agency information as a JSON"""
|
||||||
|
|
||||||
# This API is only accessible to admins and analysts
|
|
||||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
|
||||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
|
||||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
|
||||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
|
||||||
|
|
||||||
agency_name = request.GET.get("agency_name")
|
agency_name = request.GET.get("agency_name")
|
||||||
agency = FederalAgency.objects.filter(agency=agency_name).first()
|
agency = FederalAgency.objects.filter(agency=agency_name).first()
|
||||||
senior_official = SeniorOfficial.objects.filter(federal_agency=agency).first()
|
senior_official = SeniorOfficial.objects.filter(federal_agency=agency).first()
|
||||||
|
@ -37,16 +31,10 @@ def get_senior_official_from_federal_agency_json(request):
|
||||||
return JsonResponse({"error": "Senior Official not found"}, status=404)
|
return JsonResponse({"error": "Senior Official not found"}, status=404)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||||
def get_portfolio_json(request):
|
def get_portfolio_json(request):
|
||||||
"""Returns portfolio information as a JSON"""
|
"""Returns portfolio information as a JSON"""
|
||||||
|
|
||||||
# This API is only accessible to admins and analysts
|
|
||||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
|
||||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
|
||||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
|
||||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
|
||||||
|
|
||||||
portfolio_id = request.GET.get("id")
|
portfolio_id = request.GET.get("id")
|
||||||
try:
|
try:
|
||||||
portfolio = Portfolio.objects.get(id=portfolio_id)
|
portfolio = Portfolio.objects.get(id=portfolio_id)
|
||||||
|
@ -93,16 +81,10 @@ def get_portfolio_json(request):
|
||||||
return JsonResponse(portfolio_dict)
|
return JsonResponse(portfolio_dict)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||||
def get_suborganization_list_json(request):
|
def get_suborganization_list_json(request):
|
||||||
"""Returns suborganization list information for a portfolio as a JSON"""
|
"""Returns suborganization list information for a portfolio as a JSON"""
|
||||||
|
|
||||||
# This API is only accessible to admins and analysts
|
|
||||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
|
||||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
|
||||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
|
||||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
|
||||||
|
|
||||||
portfolio_id = request.GET.get("portfolio_id")
|
portfolio_id = request.GET.get("portfolio_id")
|
||||||
try:
|
try:
|
||||||
portfolio = Portfolio.objects.get(id=portfolio_id)
|
portfolio = Portfolio.objects.get(id=portfolio_id)
|
||||||
|
@ -115,17 +97,11 @@ def get_suborganization_list_json(request):
|
||||||
return JsonResponse({"results": results, "pagination": {"more": False}})
|
return JsonResponse({"results": results, "pagination": {"more": False}})
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||||
def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
||||||
"""Returns specific portfolio information as a JSON. Request must have
|
"""Returns specific portfolio information as a JSON. Request must have
|
||||||
both agency_name and organization_type."""
|
both agency_name and organization_type."""
|
||||||
|
|
||||||
# This API is only accessible to admins and analysts
|
|
||||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
|
||||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
|
||||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
|
||||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
|
||||||
|
|
||||||
federal_type = None
|
federal_type = None
|
||||||
portfolio_type = None
|
portfolio_type = None
|
||||||
|
|
||||||
|
@ -143,16 +119,10 @@ def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
||||||
return JsonResponse(response_data)
|
return JsonResponse(response_data)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||||
def get_action_needed_email_for_user_json(request):
|
def get_action_needed_email_for_user_json(request):
|
||||||
"""Returns a default action needed email for a given user"""
|
"""Returns a default action needed email for a given user"""
|
||||||
|
|
||||||
# This API is only accessible to admins and analysts
|
|
||||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
|
||||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
|
||||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
|
||||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
|
||||||
|
|
||||||
reason = request.GET.get("reason")
|
reason = request.GET.get("reason")
|
||||||
domain_request_id = request.GET.get("domain_request_id")
|
domain_request_id = request.GET.get("domain_request_id")
|
||||||
if not reason:
|
if not reason:
|
||||||
|
@ -167,16 +137,10 @@ def get_action_needed_email_for_user_json(request):
|
||||||
return JsonResponse({"email": email}, status=200)
|
return JsonResponse({"email": email}, status=200)
|
||||||
|
|
||||||
|
|
||||||
@grant_access(IS_STAFF)
|
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||||
def get_rejection_email_for_user_json(request):
|
def get_rejection_email_for_user_json(request):
|
||||||
"""Returns a default rejection email for a given user"""
|
"""Returns a default rejection email for a given user"""
|
||||||
|
|
||||||
# This API is only accessible to admins and analysts
|
|
||||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
|
||||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
|
||||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
|
||||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
|
||||||
|
|
||||||
reason = request.GET.get("reason")
|
reason = request.GET.get("reason")
|
||||||
domain_request_id = request.GET.get("domain_request_id")
|
domain_request_id = request.GET.get("domain_request_id")
|
||||||
if not reason:
|
if not reason:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue