mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-28 21:46:33 +02:00
Merge remote-tracking branch 'origin/main' into ms/3212-FEB-questions
This commit is contained in:
commit
ae03ec2314
37 changed files with 2115 additions and 484 deletions
|
@ -207,6 +207,17 @@ Linters:
|
|||
docker-compose exec app ./manage.py lint
|
||||
```
|
||||
|
||||
### Get availability for domain requests to work locally
|
||||
|
||||
If you're on local (localhost:8080) and want to submit a domain request, and keep getting the "We’re experiencing a system error. Please wait a few minutes and try again. If you continue to get this error, contact help@get.gov." error, you can get past the availability check by updating the available() function in registrar/models/domain.py to return True and comment everything else out - see below for reference!
|
||||
|
||||
```
|
||||
@classmethod
|
||||
def available(cls, domain: str) -> bool:
|
||||
# Comment everything else out in the function
|
||||
return True
|
||||
```
|
||||
|
||||
### Testing behind logged in pages
|
||||
|
||||
To test behind logged in pages with external tools, like `pa11y-ci` or `OWASP Zap`, add
|
||||
|
@ -305,15 +316,15 @@ You can also compile the **Sass** at any time using `npx gulp compile`. Similarl
|
|||
|
||||
We use the [CSS Block Element Modifier (BEM)](https://getbem.com/naming/) naming convention for our custom classes. This is in line with how USWDS [approaches](https://designsystem.digital.gov/whats-new/updates/2019/04/08/introducing-uswds-2-0/) their CSS class architecture and helps keep our code cohesive and readable.
|
||||
|
||||
### Upgrading USWDS and other JavaScript packages
|
||||
### Updating USWDS
|
||||
|
||||
1. Version numbers can be manually controlled in `package.json`. Edit that, if desired.
|
||||
2. Now run `docker-compose run node npm update`.
|
||||
3. Then run `docker-compose up` to recompile and recopy the assets, or run `docker-compose updateUswds` if your docker is already up.
|
||||
4. Make note of the dotgov changes in uswds-edited.js.
|
||||
5. Copy over the newly compiled code from uswds.js into uswds-edited.js.
|
||||
6. Put back the dotgov changes you made note of into uswds-edited.js.
|
||||
7. Examine the results in the running application (remember to empty your cache!) and commit `package.json` and `package-lock.json` if all is well.
|
||||
2. Now run `npx gulp updateUswds`. Refer to [official docs](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) to see what this is doing.
|
||||
3. Make note of the dotgov changes in uswds-edited.js (Ctrl-F DOTGOV for modifications to USWDS compiled code).
|
||||
4. Copy over the newly compiled code from uswds.js into uswds-edited.js.
|
||||
5. Put back the dotgov changes you made note of into uswds-edited.js.
|
||||
6. Examine the results in the running application (remember to empty your cache!) and commit `package.json` and `package-lock.json` if all is well.
|
||||
7. Read the [release notes](https://github.com/uswds/uswds/releases) for the new versions installed, note 'Breaking' and 'Markup change' and make adjustments to the code base as needed.
|
||||
|
||||
## Finite State Machines
|
||||
|
||||
|
|
292
src/package-lock.json
generated
292
src/package-lock.json
generated
|
@ -63,23 +63,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz",
|
||||
"integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
|
||||
"integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.26.8",
|
||||
"@babel/generator": "^7.26.9",
|
||||
"@babel/helper-compilation-targets": "^7.26.5",
|
||||
"@babel/helper-module-transforms": "^7.26.0",
|
||||
"@babel/helpers": "^7.26.7",
|
||||
"@babel/parser": "^7.26.8",
|
||||
"@babel/template": "^7.26.8",
|
||||
"@babel/traverse": "^7.26.8",
|
||||
"@babel/types": "^7.26.8",
|
||||
"@types/gensync": "^1.0.0",
|
||||
"@babel/helpers": "^7.26.9",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/traverse": "^7.26.9",
|
||||
"@babel/types": "^7.26.9",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
|
@ -95,14 +94,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz",
|
||||
"integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
|
||||
"integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.26.8",
|
||||
"@babel/types": "^7.26.8",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/types": "^7.26.9",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
|
@ -142,18 +141,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz",
|
||||
"integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz",
|
||||
"integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||
"@babel/helper-optimise-call-expression": "^7.25.9",
|
||||
"@babel/helper-replace-supers": "^7.25.9",
|
||||
"@babel/helper-replace-supers": "^7.26.5",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
||||
"@babel/traverse": "^7.25.9",
|
||||
"@babel/traverse": "^7.26.9",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -363,27 +362,27 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
|
||||
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
|
||||
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.9",
|
||||
"@babel/types": "^7.26.7"
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/types": "^7.26.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
|
||||
"integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
|
||||
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.8"
|
||||
"@babel/types": "^7.26.9"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
|
@ -809,13 +808,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-for-of": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
|
||||
"integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz",
|
||||
"integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9",
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -1376,9 +1375,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/preset-env": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.8.tgz",
|
||||
"integrity": "sha512-um7Sy+2THd697S4zJEfv/U5MHGJzkN2xhtsR3T/SWRbVSic62nbISh51VVfU9JiO/L/Z97QczHTaFVkOU8IzNg==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz",
|
||||
"integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -1411,7 +1410,7 @@
|
|||
"@babel/plugin-transform-dynamic-import": "^7.25.9",
|
||||
"@babel/plugin-transform-exponentiation-operator": "^7.26.3",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.25.9",
|
||||
"@babel/plugin-transform-for-of": "^7.25.9",
|
||||
"@babel/plugin-transform-for-of": "^7.26.9",
|
||||
"@babel/plugin-transform-function-name": "^7.25.9",
|
||||
"@babel/plugin-transform-json-strings": "^7.25.9",
|
||||
"@babel/plugin-transform-literals": "^7.25.9",
|
||||
|
@ -1475,9 +1474,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
|
||||
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -1488,32 +1487,32 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz",
|
||||
"integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
||||
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.26.8",
|
||||
"@babel/types": "^7.26.8"
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/types": "^7.26.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz",
|
||||
"integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
|
||||
"integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.26.8",
|
||||
"@babel/parser": "^7.26.8",
|
||||
"@babel/template": "^7.26.8",
|
||||
"@babel/types": "^7.26.8",
|
||||
"@babel/generator": "^7.26.9",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/types": "^7.26.9",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
|
@ -1522,9 +1521,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
|
||||
"integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
|
||||
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -1999,13 +1998,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/gensync": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz",
|
||||
"integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
|
@ -2014,9 +2006,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
|
||||
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
|
||||
"version": "22.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -2255,9 +2247,9 @@
|
|||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
@ -2868,9 +2860,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001699",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz",
|
||||
"integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==",
|
||||
"version": "1.0.30001702",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz",
|
||||
"integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -3142,13 +3134,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.40.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz",
|
||||
"integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==",
|
||||
"version": "3.41.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.24.3"
|
||||
"browserslist": "^4.24.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
@ -3371,9 +3363,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.97",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz",
|
||||
"integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==",
|
||||
"version": "1.5.113",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.113.tgz",
|
||||
"integrity": "sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
@ -3661,13 +3653,6 @@
|
|||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz",
|
||||
|
@ -3706,9 +3691,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
|
||||
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
@ -5718,16 +5703,6 @@
|
|||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
|
||||
|
@ -6113,9 +6088,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -6164,9 +6139,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -6186,9 +6161,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.84.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz",
|
||||
"integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==",
|
||||
"version": "1.85.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
|
||||
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
|
@ -6661,9 +6636,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/readdirp": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz",
|
||||
"integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
|
@ -6978,9 +6953,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.38.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.38.2.tgz",
|
||||
"integrity": "sha512-w8CXxxbFA5zfNsR/i8HZq5bvn18AK0O9jj7hyo1YqkovLxEFa0uP0LCVGZRqiRaKRFxXhELBp8SteeAjEnfeJg==",
|
||||
"version": "5.39.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
||||
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
|
@ -6997,9 +6972,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.11",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
|
||||
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -7229,9 +7204,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
|
||||
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -7259,16 +7234,6 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -7522,9 +7487,9 @@
|
|||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.97.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
|
||||
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
|
||||
"version": "5.98.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
|
||||
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -7546,9 +7511,9 @@
|
|||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^3.2.0",
|
||||
"schema-utils": "^4.3.0",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.1",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
|
@ -7617,59 +7582,6 @@
|
|||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webpack/node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
|
@ -7842,9 +7754,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
|
||||
"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.0.tgz",
|
||||
"integrity": "sha512-KHBC7z61OJeaMGnF3wqNZj+GGNXOyypZviiKpQeiHirG5Ib1ImwcLBH70rbMSkKfSmUNBsdf2PwaEJtKvgmkNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
|
@ -75,6 +75,19 @@ from django.utils.translation import gettext_lazy as _
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImportExportRegistrarModelAdmin(ImportExportModelAdmin):
|
||||
|
||||
def has_import_permission(self, request):
|
||||
return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm(
|
||||
"registrar.full_access_permission"
|
||||
)
|
||||
|
||||
def has_export_permission(self, request):
|
||||
return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm(
|
||||
"registrar.full_access_permission"
|
||||
)
|
||||
|
||||
|
||||
class FsmModelResource(resources.ModelResource):
|
||||
"""ModelResource is extended to support importing of tables which
|
||||
have FSMFields. ModelResource is extended with the following changes
|
||||
|
@ -465,7 +478,7 @@ class DomainRequestAdminForm(forms.ModelForm):
|
|||
# only set the available transitions if the user is not restricted
|
||||
# from editing the domain request; otherwise, the form will be
|
||||
# readonly and the status field will not have a widget
|
||||
if not domain_request.creator.is_restricted():
|
||||
if not domain_request.creator.is_restricted() and "status" in self.fields:
|
||||
self.fields["status"].widget.choices = available_transitions
|
||||
|
||||
def get_custom_field_transitions(self, instance, field):
|
||||
|
@ -919,7 +932,7 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
|
|||
return filters
|
||||
|
||||
|
||||
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||
class MyUserAdmin(BaseUserAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom user admin class to use our inlines."""
|
||||
|
||||
resource_classes = [UserResource]
|
||||
|
@ -1224,7 +1237,7 @@ class HostResource(resources.ModelResource):
|
|||
model = models.Host
|
||||
|
||||
|
||||
class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||
class MyHostAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom host admin class to use our inlines."""
|
||||
|
||||
resource_classes = [HostResource]
|
||||
|
@ -1242,7 +1255,7 @@ class HostIpResource(resources.ModelResource):
|
|||
model = models.HostIP
|
||||
|
||||
|
||||
class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||
class HostIpAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom host ip admin class"""
|
||||
|
||||
resource_classes = [HostIpResource]
|
||||
|
@ -1257,7 +1270,7 @@ class ContactResource(resources.ModelResource):
|
|||
model = models.Contact
|
||||
|
||||
|
||||
class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class ContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom contact admin class to add search."""
|
||||
|
||||
resource_classes = [ContactResource]
|
||||
|
@ -1391,6 +1404,59 @@ class SeniorOfficialAdmin(ListHeaderAdmin):
|
|||
# in autocomplete_fields for Senior Official
|
||||
ordering = ["first_name", "last_name"]
|
||||
|
||||
readonly_fields = []
|
||||
|
||||
# Even though this is empty, I will leave it as a stub for easy changes in the future
|
||||
# rather than strip it out of our logic.
|
||||
analyst_readonly_fields = [] # type: ignore
|
||||
|
||||
omb_analyst_readonly_fields = [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"title",
|
||||
"phone",
|
||||
"email",
|
||||
"federal_agency",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have conditions that determine which fields are read-only:
|
||||
admin user permissions and analyst (cisa or omb) status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return obj.federal_agency and obj.federal_agency.federal_type == BranchChoices.EXECUTIVE
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
|
||||
class WebsiteResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -1400,7 +1466,7 @@ class WebsiteResource(resources.ModelResource):
|
|||
model = models.Website
|
||||
|
||||
|
||||
class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class WebsiteAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom website admin class."""
|
||||
|
||||
resource_classes = [WebsiteResource]
|
||||
|
@ -1501,7 +1567,7 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
|||
obj.delete() # Calls the overridden delete method on each instance
|
||||
|
||||
|
||||
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom user domain role admin class."""
|
||||
|
||||
resource_classes = [UserDomainRoleResource]
|
||||
|
@ -1684,6 +1750,63 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||
delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html"
|
||||
|
||||
def get_annotated_queryset(self, queryset):
|
||||
return queryset.annotate(
|
||||
converted_generic_org_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
domain__domain_info__portfolio__isnull=False,
|
||||
then=F("domain__domain_info__portfolio__organization_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("domain__domain_info__generic_org_type"),
|
||||
),
|
||||
converted_federal_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
Q(domain__domain_info__portfolio__isnull=False)
|
||||
& Q(domain__domain_info__portfolio__federal_agency__isnull=False),
|
||||
then=F("domain__domain_info__portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the federal agency's federal_type
|
||||
default=F("domain__domain_info__federal_agency__federal_type"),
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
annotated_qs = self.get_annotated_queryset(qs)
|
||||
return annotated_qs.filter(
|
||||
converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
converted_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.domain.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.domain.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
# Select domain invitations to change -> Domain invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Domain invitations"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Override the change_view to add the invitation obj for the change_form_object_tools template"""
|
||||
|
||||
|
@ -1846,7 +1969,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
requested_user = get_requested_user(requested_email)
|
||||
|
||||
permission_exists = UserPortfolioPermission.objects.filter(
|
||||
user__email=requested_email, portfolio=portfolio, user__email__isnull=False
|
||||
user__email__iexact=requested_email, portfolio=portfolio, user__email__isnull=False
|
||||
).exists()
|
||||
if not permission_exists:
|
||||
# if permission does not exist for a user with requested_email, send email
|
||||
|
@ -1856,9 +1979,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
portfolio=portfolio,
|
||||
is_admin_invitation=is_admin_invitation,
|
||||
):
|
||||
messages.warning(
|
||||
self.request, "Could not send email notification to existing organization admins."
|
||||
)
|
||||
messages.warning(request, "Could not send email notification to existing organization admins.")
|
||||
# if user exists for email, immediately retrieve portfolio invitation upon creation
|
||||
if requested_user is not None:
|
||||
obj.retrieve()
|
||||
|
@ -1907,7 +2028,7 @@ class DomainInformationResource(resources.ModelResource):
|
|||
model = models.DomainInformation
|
||||
|
||||
|
||||
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainInformationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Customize domain information admin class."""
|
||||
|
||||
class GenericOrgFilter(admin.SimpleListFilter):
|
||||
|
@ -2184,6 +2305,47 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"is_policy_acknowledged",
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for OMB Analysts
|
||||
omb_analyst_readonly_fields = [
|
||||
"federal_agency",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"anything_else",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"domain_request",
|
||||
"notes",
|
||||
"senior_official",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
"portfolio_organization_type",
|
||||
"portfolio_federal_type",
|
||||
"portfolio_organization_name",
|
||||
"portfolio_federal_agency",
|
||||
"portfolio_state_territory",
|
||||
"portfolio_address_line1",
|
||||
"portfolio_address_line2",
|
||||
"portfolio_city",
|
||||
"portfolio_zipcode",
|
||||
"portfolio_urbanization",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"about_your_organization",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
]
|
||||
|
||||
# For each filter_horizontal, init in admin js initFilterHorizontalWidget
|
||||
# to activate the edit/delete/view buttons
|
||||
filter_horizontal = ("other_contacts",)
|
||||
|
@ -2212,6 +2374,10 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
|
@ -2228,6 +2394,38 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
use_sort = db_field.name != "senior_official"
|
||||
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
||||
|
||||
def get_annotated_queryset(self, queryset):
|
||||
return queryset.annotate(
|
||||
conv_generic_org_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("generic_org_type"),
|
||||
),
|
||||
conv_federal_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the federal_type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||
request params."""
|
||||
qs = super().get_queryset(request)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
annotated_qs = self.get_annotated_queryset(qs)
|
||||
return annotated_qs.filter(
|
||||
conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
conv_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class DomainRequestResource(FsmModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -2237,7 +2435,7 @@ class DomainRequestResource(FsmModelResource):
|
|||
model = models.DomainRequest
|
||||
|
||||
|
||||
class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom domain requests admin class."""
|
||||
|
||||
resource_classes = [DomainRequestResource]
|
||||
|
@ -2295,7 +2493,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
class FederalTypeFilter(admin.SimpleListFilter):
|
||||
"""Custom Federal Type filter that accomodates portfolio feature.
|
||||
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||
organization in the Domain Request object."""
|
||||
organization in the Domain Request object's federal agency."""
|
||||
|
||||
title = "federal type"
|
||||
parameter_name = "converted_federal_types"
|
||||
|
@ -2336,7 +2534,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if self.value():
|
||||
return queryset.filter(
|
||||
Q(portfolio__federal_agency__federal_type=self.value())
|
||||
| Q(portfolio__isnull=True, federal_type=self.value())
|
||||
| Q(portfolio__isnull=True, federal_agency__federal_type=self.value())
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
@ -2751,6 +2949,62 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"cisa_representative_email",
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for OMB Analysts
|
||||
omb_analyst_readonly_fields = [
|
||||
"federal_agency",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"status",
|
||||
"investigator",
|
||||
"notes",
|
||||
"senior_official",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
"portfolio_organization_type",
|
||||
"portfolio_federal_type",
|
||||
"portfolio_organization_name",
|
||||
"portfolio_federal_agency",
|
||||
"portfolio_state_territory",
|
||||
"portfolio_address_line1",
|
||||
"portfolio_address_line2",
|
||||
"portfolio_city",
|
||||
"portfolio_zipcode",
|
||||
"portfolio_urbanization",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"about_your_organization",
|
||||
"rejection_reason",
|
||||
"rejection_reason_email",
|
||||
"action_needed_reason",
|
||||
"action_needed_reason_email",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
]
|
||||
|
||||
autocomplete_fields = [
|
||||
"approved_domain",
|
||||
"requested_domain",
|
||||
|
@ -2991,6 +3245,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
|
@ -3174,6 +3432,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
use_sort = db_field.name != "senior_official"
|
||||
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
||||
|
||||
def get_annotated_queryset(self, queryset):
|
||||
return queryset.annotate(
|
||||
conv_generic_org_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("generic_org_type"),
|
||||
),
|
||||
conv_federal_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return federal type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||
request params."""
|
||||
|
@ -3183,8 +3460,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if portfolio_id:
|
||||
# Further filter the queryset by the portfolio
|
||||
qs = qs.filter(portfolio=portfolio_id)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
annotated_qs = self.get_annotated_queryset(qs)
|
||||
return annotated_qs.filter(
|
||||
conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
conv_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Restrict update permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_change_permission(request, obj)
|
||||
|
||||
def get_search_results(self, request, queryset, search_term):
|
||||
# Call the parent's method to apply default search logic
|
||||
base_queryset, use_distinct = super().get_search_results(request, queryset, search_term)
|
||||
|
@ -3204,6 +3512,15 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
return combined_queryset, use_distinct
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
form.show_contact_as_plain_text = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class TransitionDomainAdmin(ListHeaderAdmin):
|
||||
"""Custom transition domain admin class."""
|
||||
|
@ -3235,6 +3552,16 @@ class DomainInformationInline(admin.StackedInline):
|
|||
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||
model = models.DomainInformation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the admin class and define a default value for is_omb_analyst."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_omb_analyst = False # Default value in case it's accessed before being set
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Ensure self.is_omb_analyst is set early."""
|
||||
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
return super().get_queryset(request)
|
||||
|
||||
# Define methods to display fields from the related portfolio
|
||||
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
|
||||
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
|
||||
|
@ -3302,6 +3629,7 @@ class DomainInformationInline(admin.StackedInline):
|
|||
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
|
||||
readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields)
|
||||
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
|
||||
omb_analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.omb_analyst_readonly_fields)
|
||||
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
|
||||
|
||||
def get_domain_managers(self, obj):
|
||||
|
@ -3322,12 +3650,16 @@ class DomainInformationInline(admin.StackedInline):
|
|||
if not domain_managers:
|
||||
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:
|
||||
full_name = domain_manager.get_formatted_name()
|
||||
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
|
||||
domain_manager_details += "<tr>"
|
||||
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
|
||||
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>{escape(full_name)}</td>"
|
||||
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
|
||||
domain_manager_details += "</tr>"
|
||||
|
@ -3359,7 +3691,8 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if analyst_perm and not superuser_perm:
|
||||
omb_analyst_perm = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
if (analyst_perm or omb_analyst_perm) and not superuser_perm:
|
||||
return True
|
||||
return super().has_change_permission(request, obj)
|
||||
|
||||
|
@ -3433,6 +3766,23 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
return modified_fieldsets
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
form.show_contact_as_plain_text = self.is_omb_analyst
|
||||
form.is_omb_analyst = self.is_omb_analyst
|
||||
|
||||
return form
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
"""Attach request to the formset so that it can be available in the form"""
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
formset.form.request = request # Attach request to form
|
||||
return formset
|
||||
|
||||
|
||||
class DomainResource(FsmModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -3442,7 +3792,7 @@ class DomainResource(FsmModelResource):
|
|||
model = models.Domain
|
||||
|
||||
|
||||
class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom domain admin class to add extra buttons."""
|
||||
|
||||
resource_classes = [DomainResource]
|
||||
|
@ -3554,7 +3904,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if self.value():
|
||||
return queryset.filter(
|
||||
Q(domain_info__portfolio__federal_type=self.value())
|
||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
|
||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
@ -3581,7 +3931,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
|
||||
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
# Otherwise, return federal type from federal agency
|
||||
default=F("domain_info__federal_agency__federal_type"),
|
||||
),
|
||||
converted_organization_name=Case(
|
||||
|
@ -4008,8 +4358,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Fixes a bug wherein users which are only is_staff
|
||||
# can access 'change' when GET,
|
||||
# but cannot access this page when it is a request of type POST.
|
||||
if request.user.has_perm("registrar.full_access_permission") or request.user.has_perm(
|
||||
"registrar.analyst_access_permission"
|
||||
if (
|
||||
request.user.has_perm("registrar.full_access_permission")
|
||||
or request.user.has_perm("registrar.analyst_access_permission")
|
||||
or request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
):
|
||||
return True
|
||||
return super().has_change_permission(request, obj)
|
||||
|
@ -4024,8 +4376,37 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if portfolio_id:
|
||||
# Further filter the queryset by the portfolio
|
||||
qs = qs.filter(domain_info__portfolio=portfolio_id)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(
|
||||
converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
converted_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
form.show_contact_as_plain_text = is_omb_analyst
|
||||
form.is_omb_analyst = is_omb_analyst
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class DraftDomainResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -4035,7 +4416,7 @@ class DraftDomainResource(resources.ModelResource):
|
|||
model = models.DraftDomain
|
||||
|
||||
|
||||
class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DraftDomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom draft domain admin class."""
|
||||
|
||||
resource_classes = [DraftDomainResource]
|
||||
|
@ -4147,7 +4528,7 @@ class PublicContactResource(resources.ModelResource):
|
|||
self.after_save_instance(instance, using_transactions, dry_run)
|
||||
|
||||
|
||||
class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class PublicContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom PublicContact admin class."""
|
||||
|
||||
resource_classes = [PublicContactResource]
|
||||
|
@ -4202,6 +4583,11 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
_meta = Meta()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the admin class and define a default value for is_omb_analyst."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_omb_analyst = False # Default value in case it's accessed before being set
|
||||
|
||||
change_form_template = "django/admin/portfolio_change_form.html"
|
||||
fieldsets = [
|
||||
# created_on is the created_at field
|
||||
|
@ -4283,6 +4669,19 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
# rather than strip it out of our logic.
|
||||
analyst_readonly_fields = [] # type: ignore
|
||||
|
||||
omb_analyst_readonly_fields = [
|
||||
"notes",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"federal_agency",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
|
||||
def get_admin_users(self, obj):
|
||||
# Filter UserPortfolioPermission objects related to the portfolio
|
||||
admin_permissions = self.get_user_portfolio_permission_admins(obj)
|
||||
|
@ -4368,6 +4767,8 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
"""Returns the number of administrators for this portfolio"""
|
||||
admin_count = len(self.get_user_portfolio_permission_admins(obj))
|
||||
if admin_count > 0:
|
||||
if self.is_omb_analyst:
|
||||
return format_html(f"{admin_count} administrators")
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{admin_count} admins</a>')
|
||||
|
@ -4379,6 +4780,8 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
"""Returns the number of basic members for this portfolio"""
|
||||
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
|
||||
if member_count > 0:
|
||||
if self.is_omb_analyst:
|
||||
return format_html(f"{member_count} members")
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{member_count} basic members</a>')
|
||||
|
@ -4424,12 +4827,35 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
self.is_omb_analyst = True
|
||||
return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return obj.federal_type == BranchChoices.EXECUTIVE
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Add related suborganizations and domain groups.
|
||||
Add the summary for the portfolio members field (list of members that link to change_forms)."""
|
||||
|
@ -4474,6 +4900,17 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
form.show_contact_as_plain_text = self.is_omb_analyst
|
||||
form.is_omb_analyst = self.is_omb_analyst
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class FederalAgencyResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -4483,13 +4920,66 @@ class FederalAgencyResource(resources.ModelResource):
|
|||
model = models.FederalAgency
|
||||
|
||||
|
||||
class FederalAgencyAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
list_display = ["agency"]
|
||||
search_fields = ["agency"]
|
||||
search_help_text = "Search by federal agency."
|
||||
ordering = ["agency"]
|
||||
resource_classes = [FederalAgencyResource]
|
||||
|
||||
# Readonly fields for analysts and superusers
|
||||
readonly_fields = []
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [] # type: ignore
|
||||
|
||||
# Read only that we'll leverage for OMB Analysts
|
||||
omb_analyst_readonly_fields = [
|
||||
"agency",
|
||||
"federal_type",
|
||||
"acronym",
|
||||
"is_fceb",
|
||||
]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(
|
||||
federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return obj.federal_type == BranchChoices.EXECUTIVE
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 2 conditions that determine which fields are read-only:
|
||||
admin user permissions and the domain request creator's status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
|
||||
class UserGroupAdmin(AuditedAdmin):
|
||||
"""Overwrite the generated UserGroup admin class"""
|
||||
|
@ -4539,11 +5029,11 @@ class WaffleFlagAdmin(FlagAdmin):
|
|||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainGroupAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
list_display = ["name", "portfolio"]
|
||||
|
||||
|
||||
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class SuborganizationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
|
||||
list_display = ["name", "portfolio"]
|
||||
autocomplete_fields = [
|
||||
|
@ -4554,6 +5044,38 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
change_form_template = "django/admin/suborg_change_form.html"
|
||||
|
||||
readonly_fields = []
|
||||
|
||||
# Even though this is empty, I will leave it as a stub for easy changes in the future
|
||||
# rather than strip it out of our logic.
|
||||
analyst_readonly_fields = [] # type: ignore
|
||||
|
||||
omb_analyst_readonly_fields = [
|
||||
"name",
|
||||
"portfolio",
|
||||
"city",
|
||||
"state_territory",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have conditions that determine which fields are read-only:
|
||||
admin user permissions and analyst (cisa or omb) status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Add suborg's related domains and requests to context"""
|
||||
obj = self.get_object(request, object_id)
|
||||
|
@ -4571,6 +5093,30 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Custom get_queryset to filter for OMB analysts."""
|
||||
qs = super().get_queryset(request)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(
|
||||
portfolio__organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
portfolio__federal_agency__federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.portfolio
|
||||
and obj.portfolio.federal_agency
|
||||
and obj.portfolio.federal_agency.federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
|
||||
class AllowedEmailAdmin(ListHeaderAdmin):
|
||||
class Meta:
|
||||
|
|
|
@ -105,8 +105,10 @@ export function initApprovedDomain() {
|
|||
return;
|
||||
}
|
||||
|
||||
const statusToCheck = "approved";
|
||||
const statusToCheck = "approved"; // when checking against a select
|
||||
const readonlyStatusToCheck = "Approved"; // when checking against a readonly div display value
|
||||
const statusSelect = document.getElementById("id_status");
|
||||
const statusField = document.querySelector("field-status");
|
||||
const sessionVariableName = "showApprovedDomain";
|
||||
let approvedDomainFormGroup = document.querySelector(".field-approved_domain");
|
||||
|
||||
|
@ -120,18 +122,32 @@ export function initApprovedDomain() {
|
|||
|
||||
// Handle showing/hiding the related fields on page load.
|
||||
function initializeFormGroups() {
|
||||
let isStatus = statusSelect.value == statusToCheck;
|
||||
// Status is either in a select or in a readonly div. Both
|
||||
// cases are handled below.
|
||||
let isStatus = false;
|
||||
if (statusSelect) {
|
||||
isStatus = statusSelect.value == statusToCheck;
|
||||
} else {
|
||||
// statusSelect does not exist, indicating readonly
|
||||
if (statusField) {
|
||||
let readonlyDiv = statusField.querySelector("div.readonly");
|
||||
let readonlyStatusText = readonlyDiv.textContent.trim();
|
||||
isStatus = readonlyStatusText == readonlyStatusToCheck;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial handling of these groups.
|
||||
updateFormGroupVisibility(isStatus);
|
||||
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
statusSelect.addEventListener('change', () => {
|
||||
// Show the approved if the status is what we expect.
|
||||
isStatus = statusSelect.value == statusToCheck;
|
||||
updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(sessionVariableName, isStatus);
|
||||
});
|
||||
if (statusSelect) {
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
statusSelect.addEventListener('change', () => {
|
||||
// Show the approved if the status is what we expect.
|
||||
isStatus = statusSelect.value == statusToCheck;
|
||||
updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(sessionVariableName, isStatus);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to Back/Forward button navigation and handle approvedDomainFormGroup display based on session storage
|
||||
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||
|
@ -322,6 +338,7 @@ class CustomizableEmailBase {
|
|||
* @property {HTMLElement} modalConfirm - The confirm button in the modal.
|
||||
* @property {string} apiUrl - The API URL for fetching email content.
|
||||
* @property {string} statusToCheck - The status to check against. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||
* @property {string} readonlyStatusToCheck - The status to check against when readonly. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||
* @property {string} sessionVariableName - The session variable name. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||
* @property {string} apiErrorMessage - The error message that the ajax call returns.
|
||||
*/
|
||||
|
@ -338,6 +355,7 @@ class CustomizableEmailBase {
|
|||
this.textAreaFormGroup = config.textAreaFormGroup;
|
||||
this.dropdownFormGroup = config.dropdownFormGroup;
|
||||
this.statusToCheck = config.statusToCheck;
|
||||
this.readonlyStatusToCheck = config.readonlyStatusToCheck;
|
||||
this.sessionVariableName = config.sessionVariableName;
|
||||
|
||||
// Non-configurable variables
|
||||
|
@ -363,19 +381,31 @@ class CustomizableEmailBase {
|
|||
|
||||
// Handle showing/hiding the related fields on page load.
|
||||
initializeFormGroups() {
|
||||
let isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
let isStatus = false;
|
||||
if (this.statusSelect) {
|
||||
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
} else {
|
||||
// statusSelect does not exist, indicating readonly
|
||||
if (this.dropdownFormGroup) {
|
||||
let readonlyDiv = this.dropdownFormGroup.querySelector("div.readonly");
|
||||
let readonlyStatusText = readonlyDiv.textContent.trim();
|
||||
isStatus = readonlyStatusText == this.readonlyStatusToCheck;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial handling of these groups.
|
||||
this.updateFormGroupVisibility(isStatus);
|
||||
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
this.statusSelect.addEventListener('change', () => {
|
||||
// Show the action needed field if the status is what we expect.
|
||||
// Then track if its shown or hidden in our session cache.
|
||||
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
this.updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
||||
});
|
||||
if (this.statusSelect) {
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
this.statusSelect.addEventListener('change', () => {
|
||||
// Show the action needed field if the status is what we expect.
|
||||
// Then track if its shown or hidden in our session cache.
|
||||
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
this.updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
||||
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||
|
@ -403,58 +433,66 @@ class CustomizableEmailBase {
|
|||
}
|
||||
|
||||
initializeDropdown() {
|
||||
this.dropdown.addEventListener("change", () => {
|
||||
let reason = this.dropdown.value;
|
||||
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||
let searchParams = new URLSearchParams(
|
||||
{
|
||||
"reason": reason,
|
||||
"domain_request_id": this.domainRequestId,
|
||||
}
|
||||
);
|
||||
// Replace the email content
|
||||
fetch(`${this.apiUrl}?${searchParams.toString()}`)
|
||||
.then(response => {
|
||||
return response.json().then(data => data);
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error("Error in AJAX call: " + data.error);
|
||||
}else {
|
||||
this.textarea.value = data.email;
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(this.apiErrorMessage, error)
|
||||
});
|
||||
}
|
||||
});
|
||||
if (this.dropdown) {
|
||||
this.dropdown.addEventListener("change", () => {
|
||||
let reason = this.dropdown.value;
|
||||
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||
let searchParams = new URLSearchParams(
|
||||
{
|
||||
"reason": reason,
|
||||
"domain_request_id": this.domainRequestId,
|
||||
}
|
||||
);
|
||||
// Replace the email content
|
||||
fetch(`${this.apiUrl}?${searchParams.toString()}`)
|
||||
.then(response => {
|
||||
return response.json().then(data => data);
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error("Error in AJAX call: " + data.error);
|
||||
}else {
|
||||
this.textarea.value = data.email;
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(this.apiErrorMessage, error)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initializeModalConfirm() {
|
||||
this.modalConfirm.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
// When the modal confirm button is present, add a listener
|
||||
if (this.modalConfirm) {
|
||||
this.modalConfirm.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
hideElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initializeDirectEditButton() {
|
||||
this.directEditButton.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
// When the direct edit button is present, add a listener
|
||||
if (this.directEditButton) {
|
||||
this.directEditButton.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
hideElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isEmailAlreadySent() {
|
||||
return this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
updateUserInterface(reason=this.dropdown.value, excluded_reasons=["other"]) {
|
||||
updateUserInterface(reason, excluded_reasons=["other"]) {
|
||||
if (!reason) {
|
||||
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
|
||||
this.showPlaceholderNoReason();
|
||||
|
@ -468,23 +506,25 @@ class CustomizableEmailBase {
|
|||
|
||||
// Helper function that makes overriding the readonly textarea easy
|
||||
showReadonlyTextarea() {
|
||||
// A triggering selection is selected, all hands on board:
|
||||
this.textarea.setAttribute('readonly', true);
|
||||
showElement(this.textarea);
|
||||
hideElement(this.textareaPlaceholder);
|
||||
if (this.textarea && this.textareaPlaceholder) {
|
||||
// A triggering selection is selected, all hands on board:
|
||||
this.textarea.setAttribute('readonly', true);
|
||||
showElement(this.textarea);
|
||||
hideElement(this.textareaPlaceholder);
|
||||
|
||||
if (this.isEmailAlreadySentConst) {
|
||||
hideElement(this.directEditButton);
|
||||
showElement(this.modalTrigger);
|
||||
if (this.isEmailAlreadySentConst) {
|
||||
hideElement(this.directEditButton);
|
||||
showElement(this.modalTrigger);
|
||||
} else {
|
||||
showElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
}
|
||||
|
||||
if (this.isEmailAlreadySent()) {
|
||||
this.formLabel.innerHTML = "Email sent to creator:";
|
||||
} else {
|
||||
showElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
}
|
||||
|
||||
if (this.isEmailAlreadySent()) {
|
||||
this.formLabel.innerHTML = "Email sent to creator:";
|
||||
} else {
|
||||
this.formLabel.innerHTML = "Email:";
|
||||
this.formLabel.innerHTML = "Email:";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -516,9 +556,10 @@ class customActionNeededEmail extends CustomizableEmailBase {
|
|||
lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"),
|
||||
modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"),
|
||||
apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null,
|
||||
textAreaFormGroup: document.querySelector('.field-action_needed_reason'),
|
||||
dropdownFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
||||
textAreaFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
||||
dropdownFormGroup: document.querySelector('.field-action_needed_reason'),
|
||||
statusToCheck: "action needed",
|
||||
readonlyStatusToCheck: "Action needed",
|
||||
sessionVariableName: "showActionNeededReason",
|
||||
apiErrorMessage: "Error when attempting to grab action needed email: "
|
||||
}
|
||||
|
@ -529,7 +570,15 @@ class customActionNeededEmail extends CustomizableEmailBase {
|
|||
// Hide/show the email fields depending on the current status
|
||||
this.initializeFormGroups();
|
||||
// Setup the textarea, edit button, helper text
|
||||
this.updateUserInterface();
|
||||
let reason = null;
|
||||
if (this.dropdown) {
|
||||
reason = this.dropdown.value;
|
||||
} else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) {
|
||||
if (this.dropdownFormGroup.querySelector("div.readonly").textContent) {
|
||||
reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim()
|
||||
}
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
this.initializeDropdown();
|
||||
this.initializeModalConfirm();
|
||||
this.initializeDirectEditButton();
|
||||
|
@ -560,12 +609,6 @@ export function initActionNeededEmail() {
|
|||
// Initialize UI
|
||||
const customEmail = new customActionNeededEmail();
|
||||
|
||||
// Check that every variable was setup correctly
|
||||
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||
if (nullItems.length > 0) {
|
||||
console.error(`Failed to load customActionNeededEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||
return;
|
||||
}
|
||||
customEmail.loadActionNeededEmail()
|
||||
});
|
||||
}
|
||||
|
@ -581,6 +624,7 @@ class customRejectedEmail extends CustomizableEmailBase {
|
|||
textAreaFormGroup: document.querySelector('.field-rejection_reason'),
|
||||
dropdownFormGroup: document.querySelector('.field-rejection_reason_email'),
|
||||
statusToCheck: "rejected",
|
||||
readonlyStatusToCheck: "Rejected",
|
||||
sessionVariableName: "showRejectionReason",
|
||||
errorMessage: "Error when attempting to grab rejected email: "
|
||||
};
|
||||
|
@ -589,7 +633,15 @@ class customRejectedEmail extends CustomizableEmailBase {
|
|||
|
||||
loadRejectedEmail() {
|
||||
this.initializeFormGroups();
|
||||
this.updateUserInterface();
|
||||
let reason = null;
|
||||
if (this.dropdown) {
|
||||
reason = this.dropdown.value;
|
||||
} else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) {
|
||||
if (this.dropdownFormGroup.querySelector("div.readonly").textContent) {
|
||||
reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim()
|
||||
}
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
this.initializeDropdown();
|
||||
this.initializeModalConfirm();
|
||||
this.initializeDirectEditButton();
|
||||
|
@ -600,7 +652,7 @@ class customRejectedEmail extends CustomizableEmailBase {
|
|||
this.showPlaceholder("Email:", "Select a rejection reason to see email");
|
||||
}
|
||||
|
||||
updateUserInterface(reason=this.dropdown.value, excluded_reasons=[]) {
|
||||
updateUserInterface(reason, excluded_reasons=[]) {
|
||||
super.updateUserInterface(reason, excluded_reasons);
|
||||
}
|
||||
}
|
||||
|
@ -619,12 +671,6 @@ export function initRejectedEmail() {
|
|||
|
||||
// Initialize UI
|
||||
const customEmail = new customRejectedEmail();
|
||||
// Check that every variable was setup correctly
|
||||
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||
if (nullItems.length > 0) {
|
||||
console.error(`Failed to load customRejectedEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||
return;
|
||||
}
|
||||
customEmail.loadRejectedEmail()
|
||||
});
|
||||
}
|
||||
|
@ -648,7 +694,6 @@ function handleSuborgFieldsAndButtons() {
|
|||
|
||||
// Ensure that every variable is present before proceeding
|
||||
if (!requestedSuborganizationField || !suborganizationCity || !suborganizationStateTerritory || !rejectButton) {
|
||||
console.warn("handleSuborganizationSelection() => Could not find required fields.")
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ export function handlePortfolioSelection(
|
|||
suborgDropdownSelector="#id_sub_organization"
|
||||
) {
|
||||
// These dropdown are select2 fields so they must be interacted with via jquery
|
||||
// In the event that these fields are readonly, need a variable to reference their row
|
||||
const portfolioDropdown = django.jQuery(portfolioDropdownSelector);
|
||||
const portfolioField = document.querySelector(".field-portfolio");
|
||||
const suborganizationDropdown = django.jQuery(suborgDropdownSelector);
|
||||
const suborganizationField = document.querySelector(".field-sub_organization");
|
||||
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
|
||||
|
@ -394,17 +396,33 @@ export function handlePortfolioSelection(
|
|||
* - Various global field elements (e.g., `suborganizationField`, `seniorOfficialField`, `portfolioOrgTypeFieldSet`) are used.
|
||||
*/
|
||||
function updatePortfolioFieldsDisplay() {
|
||||
// Retrieve the selected portfolio ID
|
||||
let portfolio_id = portfolioDropdown.val();
|
||||
let portfolio_id = null;
|
||||
let portfolio_selected = false;
|
||||
// portfolio will be either readonly or a dropdown, handle both cases
|
||||
if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM
|
||||
// Retrieve the selected portfolio ID
|
||||
portfolio_id = portfolioDropdown.val();
|
||||
if (portfolio_id) {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
} else {
|
||||
// get readonly field value
|
||||
let portfolio = portfolioField.querySelector(".readonly").innerText;
|
||||
if (portfolio != "-") {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (portfolio_id) {
|
||||
if (portfolio_selected) {
|
||||
// A portfolio is selected - update suborganization dropdown and show/hide relevant fields
|
||||
|
||||
// Update suborganization dropdown for the selected portfolio
|
||||
updateSubOrganizationDropdown(portfolio_id);
|
||||
if (portfolio_id) {
|
||||
// Update suborganization dropdown for the selected portfolio
|
||||
updateSubOrganizationDropdown(portfolio_id);
|
||||
}
|
||||
|
||||
// Show fields relevant to a selected portfolio
|
||||
showElement(suborganizationField);
|
||||
if (suborganizationField) showElement(suborganizationField);
|
||||
hideElement(seniorOfficialField);
|
||||
showElement(portfolioSeniorOfficialField);
|
||||
|
||||
|
@ -427,7 +445,7 @@ export function handlePortfolioSelection(
|
|||
// No portfolio is selected - reverse visibility of fields
|
||||
|
||||
// Hide suborganization field as no portfolio is selected
|
||||
hideElement(suborganizationField);
|
||||
if (suborganizationField) hideElement(suborganizationField);
|
||||
|
||||
// Show fields that are relevant when no portfolio is selected
|
||||
showElement(seniorOfficialField);
|
||||
|
@ -468,10 +486,22 @@ export function handlePortfolioSelection(
|
|||
* This function ensures the form dynamically reflects whether a specific suborganization is being selected or requested.
|
||||
*/
|
||||
function updateSuborganizationFieldsDisplay() {
|
||||
let portfolio_id = portfolioDropdown.val();
|
||||
let portfolio_selected = false;
|
||||
// portfolio will be either readonly or a dropdown, handle both cases
|
||||
if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM
|
||||
// Retrieve the selected portfolio ID
|
||||
if (portfolioDropdown.val()) {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
} else {
|
||||
// get readonly field value
|
||||
if (portfolioField.querySelector(".readonly").innerText != "-") {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
}
|
||||
let suborganization_id = suborganizationDropdown.val();
|
||||
|
||||
if (portfolio_id && !suborganization_id) {
|
||||
if (portfolio_selected && !suborganization_id) {
|
||||
// Show suborganization request fields
|
||||
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
|
||||
if (suborganizationCity) showElement(suborganizationCity);
|
||||
|
|
|
@ -21,6 +21,8 @@ function handlePortfolioFields(){
|
|||
const federalTypeField = document.querySelector(".field-federal_type");
|
||||
const urbanizationField = document.querySelector(".field-urbanization");
|
||||
const stateTerritoryDropdown = document.getElementById("id_state_territory");
|
||||
const stateTerritoryField = document.querySelector(".field-state_territory");
|
||||
const stateTerritoryReadonly = stateTerritoryField.querySelector(".readonly");
|
||||
const seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
|
||||
const seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
|
||||
const federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
|
||||
|
@ -85,9 +87,9 @@ function handlePortfolioFields(){
|
|||
* 2. else show org name, hide federal agency, hide federal type if applicable
|
||||
*/
|
||||
function handleOrganizationTypeChange() {
|
||||
if (organizationTypeDropdown && organizationNameField) {
|
||||
let selectedValue = organizationTypeDropdown.value;
|
||||
if (selectedValue === "federal") {
|
||||
if (organizationTypeField && organizationNameField) {
|
||||
let selectedValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText;
|
||||
if (selectedValue === "federal" || selectedValue === "Federal") {
|
||||
hideElement(organizationNameField);
|
||||
showElement(federalAgencyField);
|
||||
if (federalTypeField) {
|
||||
|
@ -207,8 +209,8 @@ function handlePortfolioFields(){
|
|||
* Handle urbanization
|
||||
*/
|
||||
function handleStateTerritoryChange() {
|
||||
let selectedValue = stateTerritoryDropdown.value;
|
||||
if (selectedValue === "PR") {
|
||||
let selectedValue = stateTerritoryDropdown ? stateTerritoryDropdown.value : stateTerritoryReadonly.innerText;
|
||||
if (selectedValue === "PR" || selectedValue === "Puerto Rico (PR)") {
|
||||
showElement(urbanizationField)
|
||||
} else {
|
||||
hideElement(urbanizationField)
|
||||
|
@ -265,7 +267,7 @@ function handlePortfolioFields(){
|
|||
* Initializes necessary data and display configurations for the portfolio fields.
|
||||
*/
|
||||
function initializePortfolioSettings() {
|
||||
if (urbanizationField && stateTerritoryDropdown) {
|
||||
if (urbanizationField && stateTerritoryField) {
|
||||
handleStateTerritoryChange();
|
||||
}
|
||||
handleOrganizationTypeChange();
|
||||
|
@ -285,9 +287,11 @@ function handlePortfolioFields(){
|
|||
handleStateTerritoryChange();
|
||||
});
|
||||
}
|
||||
organizationTypeDropdown.addEventListener("change", function() {
|
||||
handleOrganizationTypeChange();
|
||||
});
|
||||
if (organizationTypeDropdown) {
|
||||
organizationTypeDropdown.addEventListener("change", function() {
|
||||
handleOrganizationTypeChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Run initial setup functions
|
||||
|
|
|
@ -12,6 +12,9 @@ logger = logging.getLogger(__name__)
|
|||
# Constants for clarity
|
||||
ALL = "all"
|
||||
IS_STAFF = "is_staff"
|
||||
IS_CISA_ANALYST = "is_cisa_analyst"
|
||||
IS_OMB_ANALYST = "is_omb_analyst"
|
||||
IS_FULL_ACCESS = "is_full_access"
|
||||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||
|
@ -108,6 +111,9 @@ def _user_has_permission(user, request, rules, **kwargs):
|
|||
# Define permission checks
|
||||
permission_checks = [
|
||||
(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,
|
||||
lambda: (not user.is_org_user(request) and _is_domain_manager(user, **kwargs))
|
||||
|
|
|
@ -86,7 +86,6 @@ class RequestingEntityForm(RegistrarForm):
|
|||
return {}
|
||||
# get the domain request as a dict, per usual method
|
||||
domain_request_dict = {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||
|
||||
# set sub_organization to 'other' if is_requesting_new_suborganization is True
|
||||
if isinstance(obj, DomainRequest) and obj.is_requesting_new_suborganization():
|
||||
domain_request_dict["sub_organization"] = "other"
|
||||
|
|
|
@ -22,6 +22,7 @@ from registrar.models.utility.portfolio_helper import (
|
|||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
get_portfolio_invitation_associations,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -459,7 +460,14 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm):
|
|||
if hasattr(e, "code"):
|
||||
field = "email" if "email" in self.fields else None
|
||||
if e.code == "has_existing_permissions":
|
||||
self.add_error(field, f"{self.instance.email} is already a member of another .gov organization.")
|
||||
existing_permissions, existing_invitations = get_portfolio_invitation_associations(self.instance)
|
||||
|
||||
same_portfolio_for_permissions = existing_permissions.exclude(portfolio=self.instance.portfolio)
|
||||
same_portfolio_for_invitations = existing_invitations.exclude(portfolio=self.instance.portfolio)
|
||||
if same_portfolio_for_permissions.exists() or same_portfolio_for_invitations.exists():
|
||||
self.add_error(
|
||||
field, f"{self.instance.email} is already a member of another .gov organization."
|
||||
)
|
||||
override_error = True
|
||||
elif e.code == "has_existing_invitations":
|
||||
self.add_error(
|
||||
|
|
38
src/registrar/migrations/0143_create_groups_v18.py
Normal file
38
src/registrar/migrations/0143_create_groups_v18.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0079 (which populates federal agencies)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_omb_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0142_domainrequest_feb_naming_requirements_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -245,6 +245,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
is called in the validate function on the request/domain page
|
||||
|
||||
throws- RegistryError or InvalidDomainError"""
|
||||
|
||||
if not cls.string_could_be_domain(domain):
|
||||
logger.warning("Not a valid domain: %s" % str(domain))
|
||||
# throw invalid domain error so that it can be caught in
|
||||
|
|
|
@ -449,7 +449,9 @@ class DomainInformation(TimeStampedModel):
|
|||
def converted_federal_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type
|
||||
return self.federal_type
|
||||
elif self.federal_agency:
|
||||
return self.federal_agency.federal_type
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_senior_official(self):
|
||||
|
|
|
@ -1528,7 +1528,9 @@ class DomainRequest(TimeStampedModel):
|
|||
def converted_federal_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type
|
||||
return self.federal_type
|
||||
elif self.federal_agency:
|
||||
return self.federal_agency.federal_type
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_address_line1(self):
|
||||
|
|
|
@ -141,6 +141,99 @@ class UserGroup(Group):
|
|||
except Exception as e:
|
||||
logger.error(f"Error creating analyst permissions group: {e}")
|
||||
|
||||
def create_omb_analyst_group(apps, schema_editor):
|
||||
"""This method gets run from a data migration."""
|
||||
|
||||
# Hard to pass self to these methods as the calls from migrations
|
||||
# are only expecting apps and schema_editor, so we'll just define
|
||||
# apps, schema_editor in the local scope instead
|
||||
OMB_ANALYST_GROUP_PERMISSIONS = [
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domainrequest",
|
||||
"permissions": ["change_domainrequest"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domain",
|
||||
"permissions": ["view_domain"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domaininvitation",
|
||||
"permissions": ["view_domaininvitation"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "federalagency",
|
||||
"permissions": ["view_federalagency"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "portfolio",
|
||||
"permissions": ["view_portfolio"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "suborganization",
|
||||
"permissions": ["view_suborganization"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "seniorofficial",
|
||||
"permissions": ["view_seniorofficial"],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
# of the 'atomic' block.
|
||||
# From django docs:
|
||||
# https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations
|
||||
# We can’t import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
UserGroup = apps.get_model("registrar", "UserGroup")
|
||||
|
||||
logger.info("Going to create the OMB Analyst Group")
|
||||
try:
|
||||
omb_analysts_group, _ = UserGroup.objects.get_or_create(
|
||||
name="omb_analysts_group",
|
||||
)
|
||||
|
||||
omb_analysts_group.permissions.clear()
|
||||
|
||||
for permission in OMB_ANALYST_GROUP_PERMISSIONS:
|
||||
app_label = permission["app_label"]
|
||||
model_name = permission["model"]
|
||||
permissions = permission["permissions"]
|
||||
|
||||
# Retrieve the content type for the app and model
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model_name)
|
||||
|
||||
# Retrieve the permissions based on their codenames
|
||||
permissions = Permission.objects.filter(content_type=content_type, codename__in=permissions)
|
||||
|
||||
# Assign the permissions to the group
|
||||
omb_analysts_group.permissions.add(*permissions)
|
||||
|
||||
# Convert the permissions QuerySet to a list of codenames
|
||||
permission_list = list(permissions.values_list("codename", flat=True))
|
||||
|
||||
logger.debug(
|
||||
app_label
|
||||
+ " | "
|
||||
+ model_name
|
||||
+ " | "
|
||||
+ ", ".join(permission_list)
|
||||
+ " added to group "
|
||||
+ omb_analysts_group.name
|
||||
)
|
||||
|
||||
logger.debug("OMB Analyst permissions added to group " + omb_analysts_group.name)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating analyst permissions group: {e}")
|
||||
|
||||
def create_full_access_group(apps, schema_editor):
|
||||
"""This method gets run from a data migration."""
|
||||
|
||||
|
|
|
@ -257,9 +257,6 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
Raises:
|
||||
ValidationError: If any of the validation rules are violated.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
|
||||
has_portfolio = bool(user_portfolio_permission.portfolio_id)
|
||||
portfolio_permissions = set(user_portfolio_permission._get_portfolio_permissions())
|
||||
|
||||
|
@ -286,8 +283,8 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
|
||||
# == Validate the multiple_porfolios flag. == #
|
||||
if not flag_is_active_for_user(user_portfolio_permission.user, "multiple_portfolios"):
|
||||
existing_permissions = UserPortfolioPermission.objects.exclude(id=user_portfolio_permission.id).filter(
|
||||
user=user_portfolio_permission.user
|
||||
existing_permissions, existing_invitations = get_user_portfolio_permission_associations(
|
||||
user_portfolio_permission
|
||||
)
|
||||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
|
@ -296,10 +293,6 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
code="has_existing_permissions",
|
||||
)
|
||||
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email=user_portfolio_permission.user.email).exclude(
|
||||
Q(portfolio=user_portfolio_permission.portfolio)
|
||||
| Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
if existing_invitations.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio invitation. "
|
||||
|
@ -308,6 +301,32 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
)
|
||||
|
||||
|
||||
def get_user_portfolio_permission_associations(user_portfolio_permission):
|
||||
"""
|
||||
Retrieves the associations for a user portfolio invitation.
|
||||
|
||||
Returns:
|
||||
A tuple:
|
||||
(existing_permissions, existing_invitations)
|
||||
where:
|
||||
- existing_permissions: UserPortfolioPermission objects excluding the current permission.
|
||||
- existing_invitations: PortfolioInvitation objects for the user email excluding
|
||||
the current invitation and those with status RETRIEVED.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
existing_permissions = UserPortfolioPermission.objects.exclude(id=user_portfolio_permission.id).filter(
|
||||
user=user_portfolio_permission.user
|
||||
)
|
||||
existing_invitations = PortfolioInvitation.objects.filter(
|
||||
email__iexact=user_portfolio_permission.user.email
|
||||
).exclude(
|
||||
Q(portfolio=user_portfolio_permission.portfolio)
|
||||
| Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
return (existing_permissions, existing_invitations)
|
||||
|
||||
|
||||
def validate_portfolio_invitation(portfolio_invitation):
|
||||
"""
|
||||
Validates a PortfolioInvitation instance. Located in portfolio_helper to avoid circular imports
|
||||
|
@ -324,7 +343,6 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
Raises:
|
||||
ValidationError: If any of the validation rules are violated.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -351,17 +369,12 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
)
|
||||
|
||||
# == Validate the multiple_porfolios flag. == #
|
||||
user = User.objects.filter(email=portfolio_invitation.email).first()
|
||||
user = User.objects.filter(email__iexact=portfolio_invitation.email).first()
|
||||
|
||||
# If user returns None, then we check for global assignment of multiple_portfolios.
|
||||
# Otherwise we just check on the user.
|
||||
if not flag_is_active_for_user(user, "multiple_portfolios"):
|
||||
existing_permissions = UserPortfolioPermission.objects.filter(user=user)
|
||||
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email=portfolio_invitation.email).exclude(
|
||||
Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
|
||||
existing_permissions, existing_invitations = get_portfolio_invitation_associations(portfolio_invitation)
|
||||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio. "
|
||||
|
@ -377,6 +390,27 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
)
|
||||
|
||||
|
||||
def get_portfolio_invitation_associations(portfolio_invitation):
|
||||
"""
|
||||
Retrieves the associations for a portfolio invitation.
|
||||
|
||||
Returns:
|
||||
A tuple:
|
||||
(existing_permissions, existing_invitations)
|
||||
where:
|
||||
- existing_permissions: UserPortfolioPermission objects matching the email.
|
||||
- existing_invitations: PortfolioInvitation objects for the email excluding
|
||||
the current invitation and those with status RETRIEVED.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
existing_permissions = UserPortfolioPermission.objects.filter(user__email__iexact=portfolio_invitation.email)
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email__iexact=portfolio_invitation.email).exclude(
|
||||
Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
return (existing_permissions, existing_invitations)
|
||||
|
||||
|
||||
def cleanup_after_portfolio_member_deletion(portfolio, email, user=None):
|
||||
"""
|
||||
Cleans up after removing a portfolio member or a portfolio invitation.
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if perms.registrar.analyst_access_permission or perms.full_access_permission %}
|
||||
<div class="module">
|
||||
<table class="width-full">
|
||||
<caption class="text-bold">Analytics</caption>
|
||||
|
@ -78,6 +79,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -11,13 +11,15 @@
|
|||
{% block field_sets %}
|
||||
<div class="display-flex flex-row flex-justify submit-row">
|
||||
<div class="flex-align-self-start button-list-mobile">
|
||||
{% if not adminform.form.is_omb_analyst %}
|
||||
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
||||
{# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
|
||||
<span class="mini-spacer"></span>
|
||||
<input type="submit" value="Get registry status" name="_get_status">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="desktop:flex-align-self-end">
|
||||
{% if original.state != original.State.DELETED %}
|
||||
{% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
|
||||
<a class="text-middle" href="#toggle-extend-expiration-alert" aria-controls="toggle-extend-expiration-alert" data-open-modal>
|
||||
Extend expiration date
|
||||
</a>
|
||||
|
@ -31,9 +33,11 @@
|
|||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||
{% endif %}
|
||||
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
||||
{% if not adminform.form.is_omb_analyst %}
|
||||
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||
{% endif %}
|
||||
{% if original.state != original.State.DELETED %}
|
||||
{% endif %}
|
||||
{% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
|
||||
<a class="text-middle" href="#toggle-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal>
|
||||
Remove from registry
|
||||
</a>
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
{% if show_formatted_name %}
|
||||
{% if user.get_formatted_name %}
|
||||
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ user.get_formatted_name }}
|
||||
{% else %}
|
||||
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
|
|
|
@ -69,7 +69,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% elif field.field.name == "portfolio_senior_official" %}
|
||||
<div class="readonly">
|
||||
{% if original_object.portfolio.senior_official %}
|
||||
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ field.contents|striptags }}
|
||||
{% else %}
|
||||
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No senior official found.<br>
|
||||
{% endif %}
|
||||
|
@ -78,7 +82,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% if all_contacts.count > 2 %}
|
||||
<div class="readonly">
|
||||
{% for contact in all_contacts %}
|
||||
<a href="{% url 'admin:registrar_contact_change' contact.id %}">{{ contact.get_formatted_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'admin:registrar_contact_change' contact.id %}">{{ contact.get_formatted_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -153,6 +161,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
<p>No additional members found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% elif field.field.name == "senior_official" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% else %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
{% for admin in admins %}
|
||||
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
|
||||
<tr>
|
||||
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||
{% if adminform.form.is_omb_analyst %}
|
||||
<td>{{ admin.user.get_formatted_name }}</td>
|
||||
{% else %}
|
||||
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||
{% endif %}
|
||||
<td>{{ admin.user.title }}</td>
|
||||
<td>
|
||||
{% if admin.user.email %}
|
||||
|
|
|
@ -30,6 +30,9 @@
|
|||
<a href={{ url }}>No senior official found. Create one now.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% else %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
{% if request.path|endswith:"renewal"%}
|
||||
<h1>Renew {{domain.name}} </h1>
|
||||
{%else%}
|
||||
<h1 class="break-word">Domain Overview</h1>
|
||||
<h1 class="break-word">Domain overview</h1>
|
||||
{% endif%}
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
{% if domain.dnssecdata is not None %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not enabled' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio %}
|
||||
|
|
|
@ -57,7 +57,7 @@ THANK YOU
|
|||
The .gov team
|
||||
|
||||
.Gov blog <https://get.gov/updates/>
|
||||
Domain management <{{ manage_url }}}>
|
||||
Domain management <{{ manage_url }}>
|
||||
Get.gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
|
||||
|
|
|
@ -1010,6 +1010,27 @@ def create_user(**kwargs):
|
|||
return user
|
||||
|
||||
|
||||
def create_omb_analyst_user(**kwargs):
|
||||
"""Creates a analyst user with is_staff=True and the group cisa_analysts_group"""
|
||||
User = get_user_model()
|
||||
p = "userpass"
|
||||
user = User.objects.create_user(
|
||||
username=kwargs.get("username", "ombanalystuser"),
|
||||
email=kwargs.get("email", "ombanalyst@example.com"),
|
||||
first_name=kwargs.get("first_name", "first"),
|
||||
last_name=kwargs.get("last_name", "last"),
|
||||
is_staff=kwargs.get("is_staff", True),
|
||||
title=kwargs.get("title", "title"),
|
||||
password=kwargs.get("password", p),
|
||||
phone=kwargs.get("phone", "8003111234"),
|
||||
)
|
||||
# Retrieve the group or create it if it doesn't exist
|
||||
group, _ = UserGroup.objects.get_or_create(name="omb_analysts_group")
|
||||
# Add the user to the group
|
||||
user.groups.set([group])
|
||||
return user
|
||||
|
||||
|
||||
def create_test_user():
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.utils import timezone
|
|||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from registrar import models
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from registrar.utility.email import EmailSendingError
|
||||
from registrar.utility.errors import MissingEmailError
|
||||
from waffle.testutils import override_flag
|
||||
|
@ -57,6 +58,7 @@ from .common import (
|
|||
MockDbForSharedTests,
|
||||
AuditedAdminMockData,
|
||||
completed_domain_request,
|
||||
create_omb_analyst_user,
|
||||
create_test_user,
|
||||
generic_domain_object,
|
||||
less_console_noise,
|
||||
|
@ -136,18 +138,25 @@ class TestDomainInvitationAdmin(WebTest):
|
|||
csrf_checks = False
|
||||
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.superuser = create_superuser()
|
||||
cls.site = AdminSite()
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
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.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)
|
||||
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"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.superuser)
|
||||
|
@ -159,10 +168,124 @@ class TestDomainInvitationAdmin(WebTest):
|
|||
DomainInvitation.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
Domain.objects.all().delete()
|
||||
Contact.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
|
||||
def test_has_model_description(self):
|
||||
"""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.superuser = create_superuser()
|
||||
self.testuser = create_test_user()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1148,6 +1272,26 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
|||
User.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
|
||||
def test_has_change_form_description(self):
|
||||
"""Tests if this model has a model description on the change form view"""
|
||||
|
@ -1204,6 +1348,7 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
def setUp(self):
|
||||
"""Create a client object"""
|
||||
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)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1217,6 +1362,26 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
def tearDownClass(self):
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -1791,6 +1956,8 @@ class TestHostAdmin(TestCase):
|
|||
cls.factory = RequestFactory()
|
||||
cls.admin = MyHostAdmin(model=Host, admin_site=cls.site)
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
|
||||
def setUp(self):
|
||||
"""Setup environment for a mock admin user"""
|
||||
|
@ -1806,6 +1973,20 @@ class TestHostAdmin(TestCase):
|
|||
def tearDownClass(cls):
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""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.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.mock_data_generator = AuditedAdminMockData()
|
||||
cls.test_helper = GenericTestHelper(
|
||||
factory=cls.factory,
|
||||
|
@ -1881,12 +2063,24 @@ class TestDomainInformationAdmin(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
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):
|
||||
"""Delete all Users, Domains, and UserDomainRoles"""
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
@classmethod
|
||||
|
@ -1894,6 +2088,56 @@ class TestDomainInformationAdmin(TestCase):
|
|||
User.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
|
||||
def test_domain_information_senior_official_is_alphabetically_sorted(self):
|
||||
"""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.admin = UserDomainRoleAdmin(model=UserDomainRole, admin_site=cls.site)
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.test_helper = GenericTestHelper(
|
||||
factory=cls.factory,
|
||||
user=cls.superuser,
|
||||
|
@ -2285,6 +2531,31 @@ class TestUserDomainRoleAdmin(WebTest):
|
|||
super().tearDownClass()
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""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.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
||||
def setUp(self):
|
||||
|
@ -2596,6 +2868,13 @@ class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
|||
super().tearDownClass()
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""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.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
@ -3236,6 +3516,13 @@ class TestContactAdmin(TestCase):
|
|||
super().tearDownClass()
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -3282,6 +3569,7 @@ class TestVerifiedByStaffAdmin(TestCase):
|
|||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
@ -3299,18 +3587,20 @@ class TestVerifiedByStaffAdmin(TestCase):
|
|||
super().tearDownClass()
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/verifiedbystaff/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_verifiedbystaff_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
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()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = WebsiteAdmin(model=Website, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3375,15 +3666,18 @@ class TestWebsiteAdmin(TestCase):
|
|||
Website.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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/website/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_website_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3392,13 +3686,14 @@ class TestWebsiteAdmin(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestDraftDomain(TestCase):
|
||||
class TestDraftDomainAdmin(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.admin = DraftDomainAdmin(model=DraftDomain, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
@ -3416,15 +3711,18 @@ class TestDraftDomain(TestCase):
|
|||
super().tearDownClass()
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/draftdomain/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_draftdomain_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3435,13 +3733,21 @@ class TestDraftDomain(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestFederalAgency(TestCase):
|
||||
class TestFederalAgencyAdmin(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
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.factory = RequestFactory()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
@ -3454,6 +3760,100 @@ class TestFederalAgency(TestCase):
|
|||
super().tearDownClass()
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -3471,11 +3871,12 @@ class TestFederalAgency(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestPublicContact(TestCase):
|
||||
class TestPublicContactAdmin(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = PublicContactAdmin(model=PublicContact, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3486,16 +3887,19 @@ class TestPublicContact(TestCase):
|
|||
PublicContact.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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/publiccontact/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_publiccontact_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3504,11 +3908,12 @@ class TestPublicContact(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestTransitionDomain(TestCase):
|
||||
class TestTransitionDomainAdmin(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = TransitionDomainAdmin(model=TransitionDomain, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3519,15 +3924,18 @@ class TestTransitionDomain(TestCase):
|
|||
PublicContact.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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/transitiondomain/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_transitiondomain_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3536,11 +3944,12 @@ class TestTransitionDomain(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestUserGroup(TestCase):
|
||||
class TestUserGroupAdmin(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = UserGroupAdmin(model=UserGroup, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3550,15 +3959,18 @@ class TestUserGroup(TestCase):
|
|||
super().tearDown()
|
||||
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
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/usergroup/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_usergroup_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3575,12 +3987,23 @@ class TestPortfolioAdmin(TestCase):
|
|||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
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.factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
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):
|
||||
Suborganization.objects.all().delete()
|
||||
|
@ -3588,8 +4011,118 @@ class TestPortfolioAdmin(TestCase):
|
|||
DomainRequest.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.feb_agency.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
|
||||
def test_created_on_display(self):
|
||||
"""Tests the custom created on which is a reskin of the created_at field"""
|
||||
|
@ -3777,6 +4310,7 @@ class TestTransferUser(WebTest):
|
|||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
|
@ -3797,6 +4331,13 @@ class TestTransferUser(WebTest):
|
|||
Portfolio.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
|
||||
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"""
|
||||
|
|
|
@ -17,14 +17,17 @@ from registrar.models import (
|
|||
Host,
|
||||
Portfolio,
|
||||
)
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from .common import (
|
||||
MockSESClient,
|
||||
completed_domain_request,
|
||||
less_console_noise,
|
||||
create_superuser,
|
||||
create_user,
|
||||
create_omb_analyst_user,
|
||||
create_ready_domain,
|
||||
MockEppLib,
|
||||
GenericTestHelper,
|
||||
|
@ -48,7 +51,9 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
@classmethod
|
||||
def setUpClass(self):
|
||||
super().setUpClass()
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.site = AdminSite()
|
||||
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
|
@ -56,6 +61,24 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
def setUp(self):
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.staffuser)
|
||||
self.nonfebdomain = Domain.objects.create(name="nonfebexample.com")
|
||||
self.febdomain = Domain.objects.create(name="febexample.com", state=Domain.State.READY)
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
self.portfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.staffuser,
|
||||
)
|
||||
self.domain_info = DomainInformation.objects.create(
|
||||
domain=self.febdomain, portfolio=self.portfolio, creator=self.staffuser
|
||||
)
|
||||
self.nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.staffuser,
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -65,12 +88,134 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
User.objects.all().delete()
|
||||
super().tearDownClass()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view domain list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domain_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
self.assertNotContains(response, self.nonfebdomain.name)
|
||||
self.assertNotContains(response, ">Import<")
|
||||
self.assertNotContains(response, ">Export<")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can view/edit federal executive branch domains."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.nonfebdomain.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.febdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
# test portfolio dropdown
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
self.assertNotContains(response, self.nonfebportfolio.organization_name)
|
||||
# test buttons
|
||||
self.assertNotContains(response, "Manage domain")
|
||||
self.assertNotContains(response, "Get registry status")
|
||||
self.assertNotContains(response, "Extend expiration date")
|
||||
self.assertNotContains(response, "Remove from registry")
|
||||
self.assertContains(response, "Place hold")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertNotContains(response, ">Delete<")
|
||||
# test whether fields are readonly or editable
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertNotContains(response, "id_domain_info-0-sub_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-creator")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-anything_else")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_first_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_last_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_email")
|
||||
self.assertNotContains(response, "id_domain_info-0-domain_request")
|
||||
self.assertNotContains(response, "id_domain_info-0-notes")
|
||||
self.assertNotContains(response, "id_domain_info-0-senior_official")
|
||||
self.assertNotContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-state_territory")
|
||||
self.assertNotContains(response, "id_domain_info-0-address_line1")
|
||||
self.assertNotContains(response, "id_domain_info-0-address_line2")
|
||||
self.assertNotContains(response, "id_domain_info-0-city")
|
||||
self.assertNotContains(response, "id_domain_info-0-zipcode")
|
||||
self.assertNotContains(response, "id_domain_info-0-urbanization")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_federal_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_organization_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_state_territory")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_address_line1")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_address_line2")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_city")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_zipcode")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_urbanization")
|
||||
self.assertNotContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-tribe_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-federally_recognized_tribe")
|
||||
self.assertNotContains(response, "id_domain_info-0-state_recognized_tribe")
|
||||
self.assertNotContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertNotContains(response, "id_domain_info-0-sub_organization")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_change(self):
|
||||
"""Ensure super user can view/edit all domains."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.nonfebdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.febdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
# test portfolio dropdown
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
# test buttons
|
||||
self.assertContains(response, "Manage domain")
|
||||
self.assertContains(response, "Get registry status")
|
||||
self.assertContains(response, "Extend expiration date")
|
||||
self.assertContains(response, "Remove from registry")
|
||||
self.assertContains(response, "Place hold")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, ">Delete<")
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertContains(response, "id_domain_info-0-sub_organization")
|
||||
self.assertContains(response, "id_domain_info-0-creator")
|
||||
self.assertContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertContains(response, "id_domain_info-0-anything_else")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_first_name")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_last_name")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_email")
|
||||
self.assertContains(response, "id_domain_info-0-domain_request")
|
||||
self.assertContains(response, "id_domain_info-0-notes")
|
||||
self.assertContains(response, "id_domain_info-0-senior_official")
|
||||
self.assertContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertContains(response, "id_domain_info-0-state_territory")
|
||||
self.assertContains(response, "id_domain_info-0-address_line1")
|
||||
self.assertContains(response, "id_domain_info-0-address_line2")
|
||||
self.assertContains(response, "id_domain_info-0-city")
|
||||
self.assertContains(response, "id_domain_info-0-zipcode")
|
||||
self.assertContains(response, "id_domain_info-0-urbanization")
|
||||
self.assertContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertContains(response, "id_domain_info-0-federal_type")
|
||||
self.assertContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertContains(response, "id_domain_info-0-tribe_name")
|
||||
self.assertContains(response, "id_domain_info-0-federally_recognized_tribe")
|
||||
self.assertContains(response, "id_domain_info-0-state_recognized_tribe")
|
||||
self.assertContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertContains(response, "id_domain_info-0-sub_organization")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_staff_can_see_cisa_region_federal(self):
|
||||
"""Tests if staff can see CISA Region: N/A"""
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import datetime
|
||||
from django.forms import ValidationError
|
||||
from django.utils import timezone
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from waffle.testutils import override_flag
|
||||
import re
|
||||
from django.test import RequestFactory, Client, TestCase, override_settings
|
||||
|
@ -37,6 +39,7 @@ from .common import (
|
|||
less_console_noise,
|
||||
create_superuser,
|
||||
create_user,
|
||||
create_omb_analyst_user,
|
||||
multiple_unalphabetical_domain_objects,
|
||||
MockEppLib,
|
||||
GenericTestHelper,
|
||||
|
@ -68,6 +71,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site)
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.ombanalyst = create_omb_analyst_user()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.test_helper = GenericTestHelper(
|
||||
factory=self.factory,
|
||||
|
@ -80,6 +84,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
allowed_emails = [AllowedEmail(email="mayor@igorville.gov"), AllowedEmail(email="help@get.gov")]
|
||||
AllowedEmail.objects.bulk_create(allowed_emails)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Host.objects.all().delete()
|
||||
|
@ -92,6 +102,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
SeniorOfficial.objects.all().delete()
|
||||
Suborganization.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
self.mock_client.EMAILS_SENT.clear()
|
||||
|
||||
@classmethod
|
||||
|
@ -100,6 +111,71 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
User.objects.all().delete()
|
||||
AllowedEmail.objects.all().delete()
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view domain request list."""
|
||||
febportfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebdomainrequest = completed_domain_request(
|
||||
name="test1234nonfeb.gov",
|
||||
portfolio=nonfebportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
febdomainrequest = completed_domain_request(
|
||||
name="test1234feb.gov",
|
||||
portfolio=febportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
self.client.force_login(self.ombanalyst)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, febdomainrequest.requested_domain.name)
|
||||
self.assertNotContains(response, nonfebdomainrequest.requested_domain.name)
|
||||
self.assertNotContains(response, ">Import<")
|
||||
self.assertNotContains(response, ">Export<")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can view/edit federal executive branch domain requests."""
|
||||
self.client.force_login(self.ombanalyst)
|
||||
febportfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebdomainrequest = completed_domain_request(
|
||||
name="test1234nonfeb.gov",
|
||||
portfolio=nonfebportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
febdomainrequest = completed_domain_request(
|
||||
name="test1234feb.gov",
|
||||
portfolio=febportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_change", args=[nonfebdomainrequest.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_change", args=[febdomainrequest.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, febdomainrequest.requested_domain.name)
|
||||
# test buttons
|
||||
self.assertContains(response, "Save")
|
||||
self.assertNotContains(response, ">Delete<")
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_clean_validates_duplicate_suborganization(self):
|
||||
|
@ -2076,6 +2152,86 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_readonly_fields_for_omb_analyst(self):
|
||||
with less_console_noise():
|
||||
request = self.factory.get("/") # Use the correct method and path
|
||||
request.user = self.ombanalyst
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
expected_fields = [
|
||||
"portfolio_senior_official",
|
||||
"portfolio_organization_type",
|
||||
"portfolio_federal_type",
|
||||
"portfolio_organization_name",
|
||||
"portfolio_federal_agency",
|
||||
"portfolio_state_territory",
|
||||
"portfolio_address_line1",
|
||||
"portfolio_address_line2",
|
||||
"portfolio_city",
|
||||
"portfolio_zipcode",
|
||||
"portfolio_urbanization",
|
||||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"is_election_board",
|
||||
"status_history",
|
||||
"federal_agency",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"status",
|
||||
"investigator",
|
||||
"notes",
|
||||
"senior_official",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
"portfolio_organization_type",
|
||||
"portfolio_federal_type",
|
||||
"portfolio_organization_name",
|
||||
"portfolio_federal_agency",
|
||||
"portfolio_state_territory",
|
||||
"portfolio_address_line1",
|
||||
"portfolio_address_line2",
|
||||
"portfolio_city",
|
||||
"portfolio_zipcode",
|
||||
"portfolio_urbanization",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"about_your_organization",
|
||||
"rejection_reason",
|
||||
"rejection_reason_email",
|
||||
"action_needed_reason",
|
||||
"action_needed_reason_email",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_saving_when_restricted_creator(self):
|
||||
with less_console_noise():
|
||||
# Create an instance of the model
|
||||
|
|
|
@ -72,7 +72,7 @@ class CsvReportsTest(MockDbForSharedTests):
|
|||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
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("adomain10.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()
|
||||
expected_file_content = [
|
||||
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("adomain10.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,"
|
||||
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||
'"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),,"
|
||||
"squeaker@rocks.com\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"
|
||||
"xdomain7.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"
|
||||
)
|
||||
|
||||
|
@ -498,7 +498,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"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"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\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
|
||||
expected_content = (
|
||||
"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"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\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"
|
||||
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Portfolio1FederalAgency,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"
|
||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\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"
|
||||
"\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",'
|
||||
"woofwardthethird@rocks.com\n"
|
||||
"zdomain12.gov,Interstate,meoward@rocks.com,\n"
|
||||
|
@ -716,7 +716,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
expected_content = (
|
||||
"Domain request,Domain type,Federal type\n"
|
||||
"city3.gov,Federal,Executive\n"
|
||||
"city4.gov,City,Executive\n"
|
||||
"city4.gov,City,\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,"
|
||||
"CISA regional representative,Current websites,Investigator\n"
|
||||
# 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,"
|
||||
"Testy Tester testy2@town.com,,city.com,\n"
|
||||
"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, '
|
||||
'Testy Tester testy2@town.com",'
|
||||
'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,"
|
||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||
"Testy Tester testy2@town.com,"
|
||||
|
|
|
@ -3930,17 +3930,59 @@ class TestPortfolioInviteNewMemberView(MockEppLib, WebTest):
|
|||
response = self.client.post(
|
||||
reverse("new-member"),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
|
||||
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
"email": self.user.email,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
with open("debug_response.html", "w") as f:
|
||||
f.write(response.content.decode("utf-8"))
|
||||
|
||||
# Verify messages
|
||||
self.assertContains(
|
||||
response,
|
||||
f"{self.user.email} is already a member of another .gov organization.",
|
||||
"User is already a member of this portfolio.",
|
||||
)
|
||||
|
||||
# Validate Database has not changed
|
||||
invite_count_after = PortfolioInvitation.objects.count()
|
||||
self.assertEqual(invite_count_after, invite_count_before)
|
||||
|
||||
# assert that send_portfolio_invitation_email is not called
|
||||
mock_send_email.assert_not_called()
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
|
||||
def test_member_invite_for_existing_member_uppercase(self, mock_send_email):
|
||||
"""Tests the member invitation flow for existing portfolio member with a different case."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# Simulate a session to ensure continuity
|
||||
session_id = self.client.session.session_key
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
invite_count_before = PortfolioInvitation.objects.count()
|
||||
|
||||
# Simulate submission of member invite for user who has already been invited
|
||||
response = self.client.post(
|
||||
reverse("new-member"),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
"email": self.user.email.upper(),
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
with open("debug_response.html", "w") as f:
|
||||
f.write(response.content.decode("utf-8"))
|
||||
|
||||
# Verify messages
|
||||
self.assertContains(
|
||||
response,
|
||||
"User is already a member of this portfolio.",
|
||||
)
|
||||
|
||||
# Validate Database has not changed
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from registrar.models.domain_request import DomainRequest
|
||||
from django.conf import settings
|
||||
from django.template.loader import get_template
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
|
@ -35,8 +36,13 @@ def _get_default_email(domain_request, file_path, reason, excluded_reasons=None)
|
|||
return None
|
||||
|
||||
recipient = domain_request.creator
|
||||
env_base_url = settings.BASE_URL
|
||||
# If NOT in prod, update instances of "manage.get.gov" links to point to
|
||||
# current environment, ie "getgov-rh.app.cloud.gov"
|
||||
manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov"
|
||||
|
||||
# Return the context of the rendered views
|
||||
context = {"domain_request": domain_request, "recipient": recipient, "reason": reason}
|
||||
context = {"domain_request": domain_request, "recipient": recipient, "reason": reason, "manage_url": manage_url}
|
||||
|
||||
email_body_text = get_template(file_path).render(context=context)
|
||||
email_body_text_cleaned = email_body_text.strip().lstrip("\n") if email_body_text else None
|
||||
|
|
|
@ -579,8 +579,8 @@ class DomainExport(BaseExport):
|
|||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("federal_type"),
|
||||
# Otherwise, return the federal type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
output_field=CharField(),
|
||||
),
|
||||
"converted_organization_name": Case(
|
||||
|
@ -1654,8 +1654,8 @@ class DomainRequestExport(BaseExport):
|
|||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("federal_type"),
|
||||
# Otherwise, return the federal type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
output_field=CharField(),
|
||||
),
|
||||
"converted_organization_name": Case(
|
||||
|
|
|
@ -227,7 +227,6 @@ class DomainRequestWizard(TemplateView):
|
|||
creator=self.request.user,
|
||||
portfolio=portfolio,
|
||||
)
|
||||
|
||||
# Question for reviewers: we should probably be doing this right?
|
||||
if portfolio and not self._domain_request.generic_org_type:
|
||||
self._domain_request.generic_org_type = portfolio.organization_type
|
||||
|
@ -598,7 +597,6 @@ class RequestingEntity(DomainRequestWizard):
|
|||
"suborganization_state_territory": None,
|
||||
}
|
||||
)
|
||||
|
||||
super().save(forms)
|
||||
|
||||
|
||||
|
@ -997,11 +995,9 @@ class Finished(DomainRequestWizard):
|
|||
forms = [] # type: ignore
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data()
|
||||
context["domain_request_id"] = self.domain_request.id
|
||||
# clean up this wizard session, because we are done with it
|
||||
del self.storage
|
||||
return render(self.request, self.template_name, context)
|
||||
return render(self.request, self.template_name)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||
|
|
|
@ -970,7 +970,7 @@ class PortfolioAddMemberView(DetailView, FormMixin):
|
|||
portfolio = form.cleaned_data["portfolio"]
|
||||
is_admin_invitation = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in form.cleaned_data["roles"]
|
||||
|
||||
requested_user = User.objects.filter(email=requested_email).first()
|
||||
requested_user = User.objects.filter(email__iexact=requested_email).first()
|
||||
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=portfolio).exists()
|
||||
try:
|
||||
if not requested_user or not permission_exists:
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.shortcuts import render
|
|||
from django.contrib import admin
|
||||
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
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
|
@ -16,7 +16,7 @@ import logging
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class AnalyticsView(View):
|
||||
def get(self, request):
|
||||
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||
|
@ -176,7 +176,7 @@ class AnalyticsView(View):
|
|||
return render(request, "admin/analytics.html", context)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataType(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# match the CSV example with all the fields
|
||||
|
@ -227,7 +227,7 @@ class ExportMembersPortfolio(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataFull(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Smaller export based on 1
|
||||
|
@ -237,7 +237,7 @@ class ExportDataFull(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataFederal(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Federal only
|
||||
|
@ -247,7 +247,7 @@ class ExportDataFederal(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDomainRequestDataFull(View):
|
||||
"""Generates a downloaded report containing all Domain Requests (except started)"""
|
||||
|
||||
|
@ -259,7 +259,7 @@ class ExportDomainRequestDataFull(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataDomainsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -272,7 +272,7 @@ class ExportDataDomainsGrowth(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataRequestsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -285,7 +285,7 @@ class ExportDataRequestsGrowth(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataManagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -297,7 +297,7 @@ class ExportDataManagedDomains(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataUnmanagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
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.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_request import DomainRequest
|
||||
from registrar.models.user import User
|
||||
|
@ -19,7 +19,7 @@ from registrar.utility.db_helpers import ignore_unique_violation
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class TransferUserView(View):
|
||||
"""Transfer user methods that set up the transfer_user template and handle the forms on it."""
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from django.http import JsonResponse
|
||||
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.utility.admin_helpers import get_action_needed_reason_default_email, get_rejection_reason_default_email
|
||||
from registrar.models.portfolio import Portfolio
|
||||
|
@ -10,16 +10,10 @@ from registrar.utility.constants import BranchChoices
|
|||
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):
|
||||
"""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 = FederalAgency.objects.filter(agency=agency_name).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)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||
def get_portfolio_json(request):
|
||||
"""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")
|
||||
try:
|
||||
portfolio = Portfolio.objects.get(id=portfolio_id)
|
||||
|
@ -93,16 +81,10 @@ def get_portfolio_json(request):
|
|||
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):
|
||||
"""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")
|
||||
try:
|
||||
portfolio = Portfolio.objects.get(id=portfolio_id)
|
||||
|
@ -115,17 +97,11 @@ def get_suborganization_list_json(request):
|
|||
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):
|
||||
"""Returns specific portfolio information as a JSON. Request must have
|
||||
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
|
||||
portfolio_type = None
|
||||
|
||||
|
@ -143,16 +119,10 @@ def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
|||
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):
|
||||
"""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")
|
||||
domain_request_id = request.GET.get("domain_request_id")
|
||||
if not reason:
|
||||
|
@ -167,16 +137,10 @@ def get_action_needed_email_for_user_json(request):
|
|||
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):
|
||||
"""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")
|
||||
domain_request_id = request.GET.get("domain_request_id")
|
||||
if not reason:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue