mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-19 19:09:22 +02:00
merge main
This commit is contained in:
commit
b97cd399e0
62 changed files with 4335 additions and 1155 deletions
13
.github/workflows/deploy-development.yaml
vendored
13
.github/workflows/deploy-development.yaml
vendored
|
@ -22,9 +22,16 @@ jobs:
|
||||||
- name: Compile USWDS assets
|
- name: Compile USWDS assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: |
|
run: |
|
||||||
docker compose run node npm install &&
|
docker compose run node bash -c "\
|
||||||
docker compose run node npx gulp copyAssets &&
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
|
||||||
docker compose run node npx gulp compile
|
export NVM_DIR=\"\$HOME/.nvm\" && \
|
||||||
|
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
|
||||||
|
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
|
||||||
|
nvm install 21.7.3 && \
|
||||||
|
nvm use 21.7.3 && \
|
||||||
|
npm install && \
|
||||||
|
npx gulp copyAssets && \
|
||||||
|
npx gulp compile"
|
||||||
- name: Collect static assets
|
- name: Collect static assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: docker compose run app python manage.py collectstatic --no-input
|
run: docker compose run app python manage.py collectstatic --no-input
|
||||||
|
|
13
.github/workflows/deploy-sandbox.yaml
vendored
13
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -42,9 +42,16 @@ jobs:
|
||||||
- name: Compile USWDS assets
|
- name: Compile USWDS assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: |
|
run: |
|
||||||
docker compose run node npm install &&
|
docker compose run node bash -c "\
|
||||||
docker compose run node npx gulp copyAssets &&
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
|
||||||
docker compose run node npx gulp compile
|
export NVM_DIR=\"\$HOME/.nvm\" && \
|
||||||
|
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
|
||||||
|
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
|
||||||
|
nvm install 21.7.3 && \
|
||||||
|
nvm use 21.7.3 && \
|
||||||
|
npm install && \
|
||||||
|
npx gulp copyAssets && \
|
||||||
|
npx gulp compile"
|
||||||
- name: Collect static assets
|
- name: Collect static assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: docker compose run app python manage.py collectstatic --no-input
|
run: docker compose run app python manage.py collectstatic --no-input
|
||||||
|
|
13
.github/workflows/deploy-stable.yaml
vendored
13
.github/workflows/deploy-stable.yaml
vendored
|
@ -23,9 +23,16 @@ jobs:
|
||||||
- name: Compile USWDS assets
|
- name: Compile USWDS assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: |
|
run: |
|
||||||
docker compose run node npm install &&
|
docker compose run node bash -c "\
|
||||||
docker compose run node npx gulp copyAssets &&
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
|
||||||
docker compose run node npx gulp compile
|
export NVM_DIR=\"\$HOME/.nvm\" && \
|
||||||
|
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
|
||||||
|
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
|
||||||
|
nvm install 21.7.3 && \
|
||||||
|
nvm use 21.7.3 && \
|
||||||
|
npm install && \
|
||||||
|
npx gulp copyAssets && \
|
||||||
|
npx gulp compile"
|
||||||
- name: Collect static assets
|
- name: Collect static assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: docker compose run app python manage.py collectstatic --no-input
|
run: docker compose run app python manage.py collectstatic --no-input
|
||||||
|
|
13
.github/workflows/deploy-staging.yaml
vendored
13
.github/workflows/deploy-staging.yaml
vendored
|
@ -23,9 +23,16 @@ jobs:
|
||||||
- name: Compile USWDS assets
|
- name: Compile USWDS assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: |
|
run: |
|
||||||
docker compose run node npm install &&
|
docker compose run node bash -c "\
|
||||||
docker compose run node npx gulp copyAssets &&
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
|
||||||
docker compose run node npx gulp compile
|
export NVM_DIR=\"\$HOME/.nvm\" && \
|
||||||
|
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
|
||||||
|
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
|
||||||
|
nvm install 21.7.3 && \
|
||||||
|
nvm use 21.7.3 && \
|
||||||
|
npm install && \
|
||||||
|
npx gulp copyAssets && \
|
||||||
|
npx gulp compile"
|
||||||
- name: Collect static assets
|
- name: Collect static assets
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: docker compose run app python manage.py collectstatic --no-input
|
run: docker compose run app python manage.py collectstatic --no-input
|
||||||
|
|
|
@ -602,18 +602,18 @@ That data are synthesized from the generic_org_type field and the is_election_bo
|
||||||
The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
|
The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
|
||||||
After downloading this file, place it in `src/migrationdata`
|
After downloading this file, place it in `src/migrationdata`
|
||||||
|
|
||||||
#### Step 2: Upload the domain_election_board file to your sandbox
|
#### Step 3: Upload the domain_election_board file to your sandbox
|
||||||
Follow [Step 1: Transfer data to sandboxes](#step-1-transfer-data-to-sandboxes) and [Step 2: Transfer uploaded files to the getgov directory](#step-2-transfer-uploaded-files-to-the-getgov-directory) from the [Set Up Migrations on Sandbox](#set-up-migrations-on-sandbox) portion of this doc.
|
Follow [Step 1: Transfer data to sandboxes](#step-1-transfer-data-to-sandboxes) and [Step 2: Transfer uploaded files to the getgov directory](#step-2-transfer-uploaded-files-to-the-getgov-directory) from the [Set Up Migrations on Sandbox](#set-up-migrations-on-sandbox) portion of this doc.
|
||||||
|
|
||||||
#### Step 2: SSH into your environment
|
#### Step 4: SSH into your environment
|
||||||
```cf ssh getgov-{space}```
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
Example: `cf ssh getgov-za`
|
Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
#### Step 3: Create a shell instance
|
#### Step 5: Create a shell instance
|
||||||
```/tmp/lifecycle/shell```
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
#### Step 4: Running the script
|
#### Step 6: Running the script
|
||||||
```./manage.py populate_organization_type {domain_election_board_filename}```
|
```./manage.py populate_organization_type {domain_election_board_filename}```
|
||||||
|
|
||||||
- The domain_election_board_filename file must adhere to this format:
|
- The domain_election_board_filename file must adhere to this format:
|
||||||
|
@ -642,3 +642,29 @@ Example (assuming that this is being ran from src/):
|
||||||
| | Parameter | Description |
|
| | Parameter | Description |
|
||||||
|:-:|:------------------------------------|:-------------------------------------------------------------------|
|
|:-:|:------------------------------------|:-------------------------------------------------------------------|
|
||||||
| 1 | **domain_election_board_filename** | A file containing every domain that is an election office.
|
| 1 | **domain_election_board_filename** | A file containing every domain that is an election office.
|
||||||
|
|
||||||
|
|
||||||
|
## Populate Verification Type
|
||||||
|
This section outlines how to run the `populate_verification_type` script.
|
||||||
|
The script is used to update the verification_type field on User when it is None.
|
||||||
|
|
||||||
|
### Running on sandboxes
|
||||||
|
|
||||||
|
#### Step 1: Login to CloudFoundry
|
||||||
|
```cf login -a api.fr.cloud.gov --sso```
|
||||||
|
|
||||||
|
#### Step 2: SSH into your environment
|
||||||
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
|
Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
|
#### Step 3: Create a shell instance
|
||||||
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
|
#### Step 4: Running the script
|
||||||
|
```./manage.py populate_verification_type```
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
|
||||||
|
#### Step 1: Running the script
|
||||||
|
```docker-compose exec app ./manage.py populate_verification_type```
|
||||||
|
|
|
@ -9,7 +9,7 @@ cfenv = "*"
|
||||||
django-cors-headers = "*"
|
django-cors-headers = "*"
|
||||||
pycryptodomex = "*"
|
pycryptodomex = "*"
|
||||||
django-allow-cidr = "*"
|
django-allow-cidr = "*"
|
||||||
django-auditlog = "2.3.0"
|
django-auditlog = "*"
|
||||||
django-csp = "*"
|
django-csp = "*"
|
||||||
environs = {extras=["django"]}
|
environs = {extras=["django"]}
|
||||||
Faker = "*"
|
Faker = "*"
|
||||||
|
|
218
src/Pipfile.lock
generated
218
src/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "99cf9a4f3912639c02105889046a9eede7a29822fab6f9a04ca25f95e29513c0"
|
"sha256": "16a0db98015509322cf1d27f06fced5b7635057c4eb98921a9419d63d51925ab"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
|
@ -32,20 +32,20 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87",
|
"sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
|
||||||
"sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3"
|
"sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.88"
|
"version": "==1.34.90"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e",
|
"sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
|
||||||
"sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b"
|
"sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.88"
|
"version": "==1.34.90"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -321,12 +321,12 @@
|
||||||
},
|
},
|
||||||
"django-auditlog": {
|
"django-auditlog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7bc2c87e4aff62dec9785d1b2359a2b27148f8c286f8a52b9114fc7876c5a9f7",
|
"sha256:92db1cf4a51ceca5c26b3ff46997d9e3305a02da1bd435e2efb5b8b6d300ce1f",
|
||||||
"sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532"
|
"sha256:9de49f80a4911135d136017123cd73461f869b4947eec14d5e76db4b88182f3f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.3.0"
|
"version": "==3.0.0"
|
||||||
},
|
},
|
||||||
"django-cache-url": {
|
"django-cache-url": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1003,96 +1003,96 @@
|
||||||
},
|
},
|
||||||
"pydantic": {
|
"pydantic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352",
|
"sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5",
|
||||||
"sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"
|
"sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.7.0"
|
"version": "==2.7.1"
|
||||||
},
|
},
|
||||||
"pydantic-core": {
|
"pydantic-core": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6",
|
"sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b",
|
||||||
"sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb",
|
"sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a",
|
||||||
"sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0",
|
"sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90",
|
||||||
"sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6",
|
"sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d",
|
||||||
"sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47",
|
"sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e",
|
||||||
"sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a",
|
"sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d",
|
||||||
"sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a",
|
"sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027",
|
||||||
"sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac",
|
"sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804",
|
||||||
"sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88",
|
"sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347",
|
||||||
"sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db",
|
"sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400",
|
||||||
"sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d",
|
"sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3",
|
||||||
"sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d",
|
"sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399",
|
||||||
"sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9",
|
"sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349",
|
||||||
"sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e",
|
"sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd",
|
||||||
"sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b",
|
"sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c",
|
||||||
"sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d",
|
"sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e",
|
||||||
"sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649",
|
"sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413",
|
||||||
"sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c",
|
"sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3",
|
||||||
"sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1",
|
"sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e",
|
||||||
"sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09",
|
"sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3",
|
||||||
"sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0",
|
"sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91",
|
||||||
"sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90",
|
"sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce",
|
||||||
"sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d",
|
"sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c",
|
||||||
"sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294",
|
"sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb",
|
||||||
"sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144",
|
"sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664",
|
||||||
"sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b",
|
"sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6",
|
||||||
"sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1",
|
"sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd",
|
||||||
"sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b",
|
"sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3",
|
||||||
"sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2",
|
"sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af",
|
||||||
"sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad",
|
"sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043",
|
||||||
"sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622",
|
"sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350",
|
||||||
"sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17",
|
"sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7",
|
||||||
"sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06",
|
"sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0",
|
||||||
"sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc",
|
"sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563",
|
||||||
"sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50",
|
"sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761",
|
||||||
"sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d",
|
"sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72",
|
||||||
"sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59",
|
"sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3",
|
||||||
"sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539",
|
"sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb",
|
||||||
"sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a",
|
"sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788",
|
||||||
"sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b",
|
"sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b",
|
||||||
"sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5",
|
"sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c",
|
||||||
"sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9",
|
"sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038",
|
||||||
"sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278",
|
"sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250",
|
||||||
"sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6",
|
"sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec",
|
||||||
"sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44",
|
"sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c",
|
||||||
"sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0",
|
"sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74",
|
||||||
"sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb",
|
"sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81",
|
||||||
"sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80",
|
"sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439",
|
||||||
"sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5",
|
"sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75",
|
||||||
"sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570",
|
"sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0",
|
||||||
"sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b",
|
"sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8",
|
||||||
"sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de",
|
"sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150",
|
||||||
"sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6",
|
"sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438",
|
||||||
"sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8",
|
"sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae",
|
||||||
"sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203",
|
"sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857",
|
||||||
"sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7",
|
"sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038",
|
||||||
"sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048",
|
"sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374",
|
||||||
"sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae",
|
"sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f",
|
||||||
"sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89",
|
"sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241",
|
||||||
"sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f",
|
"sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592",
|
||||||
"sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926",
|
"sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4",
|
||||||
"sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2",
|
"sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d",
|
||||||
"sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76",
|
"sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b",
|
||||||
"sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d",
|
"sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b",
|
||||||
"sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411",
|
"sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182",
|
||||||
"sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9",
|
"sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e",
|
||||||
"sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2",
|
"sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641",
|
||||||
"sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586",
|
"sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70",
|
||||||
"sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35",
|
"sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9",
|
||||||
"sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c",
|
"sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a",
|
||||||
"sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143",
|
"sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543",
|
||||||
"sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6",
|
"sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b",
|
||||||
"sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60",
|
"sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f",
|
||||||
"sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b",
|
"sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38",
|
||||||
"sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226",
|
"sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845",
|
||||||
"sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519",
|
"sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2",
|
||||||
"sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31",
|
"sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0",
|
||||||
"sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7",
|
"sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4",
|
||||||
"sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"
|
"sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.18.1"
|
"version": "==2.18.2"
|
||||||
},
|
},
|
||||||
"pydantic-settings": {
|
"pydantic-settings": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1411,12 +1411,12 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87",
|
"sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
|
||||||
"sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3"
|
"sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.88"
|
"version": "==1.34.90"
|
||||||
},
|
},
|
||||||
"boto3-mocking": {
|
"boto3-mocking": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1429,28 +1429,28 @@
|
||||||
},
|
},
|
||||||
"boto3-stubs": {
|
"boto3-stubs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:23ca9e0cd0d3e7702d6631a1e94a4208a26b39fa6b12c734427e68a7fa649477",
|
"sha256:7361f162523168ddcfb3e0cc70e5208e78f95b9f1f2553032036a2b67ab33355",
|
||||||
"sha256:8f472d1bf09743c3d33304ecc8830d70ebe3ca19ac9604ae8da9af55421b0fce"
|
"sha256:c82f3db8558e28f766361ba1eea7c77dff735f72fef2a0b9dffaa9c0d9ae76a3"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.88"
|
"version": "==1.34.90"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e",
|
"sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
|
||||||
"sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b"
|
"sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.88"
|
"version": "==1.34.90"
|
||||||
},
|
},
|
||||||
"botocore-stubs": {
|
"botocore-stubs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:656e966ea152a4f2828892aa7a9673bc91799998f5a8efd8e8fe390f61c2f4f1",
|
"sha256:b2d7416b524bce7325aa5fe09bb5e0b6bc9531d4136f4407fa39b6bc58507f34",
|
||||||
"sha256:f55b03ae2e1706bd56299fd2975bb048f96aa49012a866e931a040a74f85c3cc"
|
"sha256:d9b66542cbb8fbe28eef3c22caf941ae22d36cc1ef55b93fc0b52239457cab57"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||||
"version": "==1.34.88"
|
"version": "==1.34.89"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1627,11 +1627,11 @@
|
||||||
},
|
},
|
||||||
"platformdirs": {
|
"platformdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
|
"sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf",
|
||||||
"sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
|
"sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==4.2.0"
|
"version": "==4.2.1"
|
||||||
},
|
},
|
||||||
"pycodestyle": {
|
"pycodestyle": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -4,8 +4,10 @@ from django.http import HttpResponse
|
||||||
from django.test import Client, TestCase, RequestFactory
|
from django.test import Client, TestCase, RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from api.tests.common import less_console_noise_decorator
|
||||||
from djangooidc.exceptions import StateMismatch, InternalError
|
from djangooidc.exceptions import StateMismatch, InternalError
|
||||||
from ..views import login_callback
|
from ..views import login_callback
|
||||||
|
from registrar.models import User, Contact, VerifiedByStaff, DomainInvitation, TransitionDomain, Domain
|
||||||
|
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
|
|
||||||
|
@ -16,6 +18,14 @@ class ViewsTest(TestCase):
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
User.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
DomainInvitation.objects.all().delete()
|
||||||
|
VerifiedByStaff.objects.all().delete()
|
||||||
|
TransitionDomain.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
|
||||||
def say_hi(*args):
|
def say_hi(*args):
|
||||||
return HttpResponse("Hi")
|
return HttpResponse("Hi")
|
||||||
|
|
||||||
|
@ -229,6 +239,140 @@ class ViewsTest(TestCase):
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.url, "/")
|
self.assertEqual(response.url, "/")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_login_callback_sets_verification_type_regular(self, mock_client):
|
||||||
|
"""
|
||||||
|
Test that openid sets the verification type to regular on the returned user.
|
||||||
|
Regular, in this context, means that this user was "Verifed by Login.gov"
|
||||||
|
"""
|
||||||
|
# SETUP
|
||||||
|
session = self.client.session
|
||||||
|
session.save()
|
||||||
|
# MOCK
|
||||||
|
# mock that callback returns user_info; this is the expected behavior
|
||||||
|
mock_client.callback.side_effect = self.user_info
|
||||||
|
# patch that the request does not require step up auth
|
||||||
|
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
|
||||||
|
"djangooidc.views._initialize_client"
|
||||||
|
) as mock_init_client:
|
||||||
|
with patch("djangooidc.views._client_is_none", return_value=True):
|
||||||
|
# TEST
|
||||||
|
# test the login callback url
|
||||||
|
response = self.client.get(reverse("openid_login_callback"))
|
||||||
|
|
||||||
|
# assert that _initialize_client was called
|
||||||
|
mock_init_client.assert_called_once()
|
||||||
|
|
||||||
|
# Assert that we get a redirect
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/")
|
||||||
|
|
||||||
|
# Test the created user object
|
||||||
|
created_user = User.objects.get(email="test@example.com")
|
||||||
|
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.REGULAR)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_login_callback_sets_verification_type_invited(self, mock_client):
|
||||||
|
"""Test that openid sets the verification type to invited on the returned user
|
||||||
|
when they exist in the DomainInvitation table"""
|
||||||
|
# SETUP
|
||||||
|
session = self.client.session
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="test123.gov")
|
||||||
|
invitation, _ = DomainInvitation.objects.get_or_create(email="test@example.com", domain=domain)
|
||||||
|
# MOCK
|
||||||
|
# mock that callback returns user_info; this is the expected behavior
|
||||||
|
mock_client.callback.side_effect = self.user_info
|
||||||
|
# patch that the request does not require step up auth
|
||||||
|
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
|
||||||
|
"djangooidc.views._initialize_client"
|
||||||
|
) as mock_init_client:
|
||||||
|
with patch("djangooidc.views._client_is_none", return_value=True):
|
||||||
|
# TEST
|
||||||
|
# test the login callback url
|
||||||
|
response = self.client.get(reverse("openid_login_callback"))
|
||||||
|
|
||||||
|
# assert that _initialize_client was called
|
||||||
|
mock_init_client.assert_called_once()
|
||||||
|
|
||||||
|
# Assert that we get a redirect
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/")
|
||||||
|
|
||||||
|
# Test the created user object
|
||||||
|
created_user = User.objects.get(email="test@example.com")
|
||||||
|
self.assertEqual(created_user.email, invitation.email)
|
||||||
|
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.INVITED)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_login_callback_sets_verification_type_grandfathered(self, mock_client):
|
||||||
|
"""Test that openid sets the verification type to grandfathered
|
||||||
|
on a user which exists in our TransitionDomain table"""
|
||||||
|
# SETUP
|
||||||
|
session = self.client.session
|
||||||
|
session.save()
|
||||||
|
# MOCK
|
||||||
|
# mock that callback returns user_info; this is the expected behavior
|
||||||
|
mock_client.callback.side_effect = self.user_info
|
||||||
|
|
||||||
|
td, _ = TransitionDomain.objects.get_or_create(username="test@example.com", domain_name="test123.gov")
|
||||||
|
|
||||||
|
# patch that the request does not require step up auth
|
||||||
|
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
|
||||||
|
"djangooidc.views._initialize_client"
|
||||||
|
) as mock_init_client:
|
||||||
|
with patch("djangooidc.views._client_is_none", return_value=True):
|
||||||
|
# TEST
|
||||||
|
# test the login callback url
|
||||||
|
response = self.client.get(reverse("openid_login_callback"))
|
||||||
|
|
||||||
|
# assert that _initialize_client was called
|
||||||
|
mock_init_client.assert_called_once()
|
||||||
|
|
||||||
|
# Assert that we get a redirect
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/")
|
||||||
|
|
||||||
|
# Test the created user object
|
||||||
|
created_user = User.objects.get(email="test@example.com")
|
||||||
|
self.assertEqual(created_user.email, td.username)
|
||||||
|
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.GRANDFATHERED)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_login_callback_sets_verification_type_verified_by_staff(self, mock_client):
|
||||||
|
"""Test that openid sets the verification type to verified_by_staff
|
||||||
|
on a user which exists in our VerifiedByStaff table"""
|
||||||
|
# SETUP
|
||||||
|
session = self.client.session
|
||||||
|
session.save()
|
||||||
|
# MOCK
|
||||||
|
# mock that callback returns user_info; this is the expected behavior
|
||||||
|
mock_client.callback.side_effect = self.user_info
|
||||||
|
|
||||||
|
vip, _ = VerifiedByStaff.objects.get_or_create(email="test@example.com")
|
||||||
|
|
||||||
|
# patch that the request does not require step up auth
|
||||||
|
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
|
||||||
|
"djangooidc.views._initialize_client"
|
||||||
|
) as mock_init_client:
|
||||||
|
with patch("djangooidc.views._client_is_none", return_value=True):
|
||||||
|
# TEST
|
||||||
|
# test the login callback url
|
||||||
|
response = self.client.get(reverse("openid_login_callback"))
|
||||||
|
|
||||||
|
# assert that _initialize_client was called
|
||||||
|
mock_init_client.assert_called_once()
|
||||||
|
|
||||||
|
# Assert that we get a redirect
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/")
|
||||||
|
|
||||||
|
# Test the created user object
|
||||||
|
created_user = User.objects.get(email="test@example.com")
|
||||||
|
self.assertEqual(created_user.email, vip.email)
|
||||||
|
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.VERIFIED_BY_STAFF)
|
||||||
|
|
||||||
def test_login_callback_no_step_up_auth(self, mock_client):
|
def test_login_callback_no_step_up_auth(self, mock_client):
|
||||||
"""Walk through login_callback when _requires_step_up_auth returns False
|
"""Walk through login_callback when _requires_step_up_auth returns False
|
||||||
and assert that we have a redirect to /"""
|
and assert that we have a redirect to /"""
|
||||||
|
|
|
@ -99,8 +99,22 @@ def login_callback(request):
|
||||||
return CLIENT.create_authn_request(request.session)
|
return CLIENT.create_authn_request(request.session)
|
||||||
user = authenticate(request=request, **userinfo)
|
user = authenticate(request=request, **userinfo)
|
||||||
if user:
|
if user:
|
||||||
|
|
||||||
|
# Fixture users kind of exist in a superposition of verification types,
|
||||||
|
# because while the system "verified" them, if they login,
|
||||||
|
# we don't know how the user themselves was verified through login.gov until
|
||||||
|
# they actually try logging in. This edge-case only matters in non-production environments.
|
||||||
|
fixture_user = User.VerificationTypeChoices.FIXTURE_USER
|
||||||
|
is_fixture_user = user.verification_type and user.verification_type == fixture_user
|
||||||
|
|
||||||
|
# Set the verification type if it doesn't already exist or if its a fixture user
|
||||||
|
if not user.verification_type or is_fixture_user:
|
||||||
|
user.set_user_verification_type()
|
||||||
|
user.save()
|
||||||
|
|
||||||
login(request, user)
|
login(request, user)
|
||||||
logger.info("Successfully logged in user %s" % user)
|
logger.info("Successfully logged in user %s" % user)
|
||||||
|
|
||||||
# Clear the flag if the exception is not caught
|
# Clear the flag if the exception is not caught
|
||||||
request.session.pop("redirect_attempted", None)
|
request.session.pop("redirect_attempted", None)
|
||||||
return redirect(request.session.get("next", "/"))
|
return redirect(request.session.get("next", "/"))
|
||||||
|
|
|
@ -108,7 +108,7 @@ services:
|
||||||
- pa11y
|
- pa11y
|
||||||
|
|
||||||
owasp:
|
owasp:
|
||||||
image: owasp/zap2docker-stable
|
image: ghcr.io/zaproxy/zaproxy:stable
|
||||||
command: zap-baseline.py -t http://app:8080 -c zap.conf -I -r zap_report.html
|
command: zap-baseline.py -t http://app:8080 -c zap.conf -I -r zap_report.html
|
||||||
volumes:
|
volumes:
|
||||||
- .:/zap/wrk/
|
- .:/zap/wrk/
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
FROM docker.io/cimg/node:current-browsers
|
FROM docker.io/cimg/node:current-browsers
|
||||||
|
FROM node:21.7.3
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install app dependencies
|
# Install app dependencies
|
||||||
|
@ -7,4 +7,6 @@ WORKDIR /app
|
||||||
# where available (npm@5+)
|
# where available (npm@5+)
|
||||||
COPY --chown=circleci:circleci package*.json ./
|
COPY --chown=circleci:circleci package*.json ./
|
||||||
|
|
||||||
|
|
||||||
|
RUN npm install -g npm@10.5.0
|
||||||
RUN npm install
|
RUN npm install
|
4
src/package-lock.json
generated
4
src/package-lock.json
generated
|
@ -15,6 +15,10 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@uswds/compile": "^1.0.0-beta.3"
|
"@uswds/compile": "^1.0.0-beta.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "21.7.3",
|
||||||
|
"npm": "10.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@gulp-sourcemaps/identity-map": {
|
"node_modules/@gulp-sourcemaps/identity-map": {
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "========================",
|
"description": "========================",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": "21.7.3",
|
||||||
|
"npm": "10.5.0"
|
||||||
|
},
|
||||||
|
"engineStrict": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pa11y-ci": "pa11y-ci",
|
"pa11y-ci": "pa11y-ci",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
|
|
@ -119,6 +119,34 @@ class MyUserAdminForm(UserChangeForm):
|
||||||
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
|
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Custom init to modify the user form"""
|
||||||
|
super(MyUserAdminForm, self).__init__(*args, **kwargs)
|
||||||
|
self._override_base_help_texts()
|
||||||
|
|
||||||
|
def _override_base_help_texts(self):
|
||||||
|
"""
|
||||||
|
Used to override pre-existing help texts in AbstractUser.
|
||||||
|
This is done to avoid modifying the base AbstractUser class.
|
||||||
|
"""
|
||||||
|
is_superuser = self.fields.get("is_superuser")
|
||||||
|
is_staff = self.fields.get("is_staff")
|
||||||
|
password = self.fields.get("password")
|
||||||
|
|
||||||
|
if is_superuser is not None:
|
||||||
|
is_superuser.help_text = "For development purposes only; provides superuser access on the database level."
|
||||||
|
|
||||||
|
if is_staff is not None:
|
||||||
|
is_staff.help_text = "Designates whether the user can log in to this admin site."
|
||||||
|
|
||||||
|
if password is not None:
|
||||||
|
# Link is copied from the base implementation of UserChangeForm.
|
||||||
|
link = f"../../{self.instance.pk}/password/"
|
||||||
|
password.help_text = (
|
||||||
|
"Raw passwords are not stored, so they will not display here. "
|
||||||
|
f'You can change the password using <a href="{link}">this form</a>.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DomainInformationAdminForm(forms.ModelForm):
|
class DomainInformationAdminForm(forms.ModelForm):
|
||||||
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||||
|
@ -582,7 +610,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
{"fields": ("username", "password", "status")},
|
{"fields": ("username", "password", "status", "verification_type")},
|
||||||
),
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
||||||
(
|
(
|
||||||
|
@ -600,13 +628,20 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readonly_fields = ("verification_type",)
|
||||||
|
|
||||||
# Hide Username (uuid), Groups and Permissions
|
# Hide Username (uuid), Groups and Permissions
|
||||||
# Q: Now that we're using Groups and Permissions,
|
# Q: Now that we're using Groups and Permissions,
|
||||||
# do we expose those to analysts to view?
|
# do we expose those to analysts to view?
|
||||||
analyst_fieldsets = (
|
analyst_fieldsets = (
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
{"fields": ("password", "status")},
|
{
|
||||||
|
"fields": (
|
||||||
|
"status",
|
||||||
|
"verification_type",
|
||||||
|
)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
||||||
(
|
(
|
||||||
|
@ -632,7 +667,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
# NOT all fields are readonly for admin, otherwise we would have
|
# NOT all fields are readonly for admin, otherwise we would have
|
||||||
# set this at the permissions level. The exception is 'status'
|
# set this at the permissions level. The exception is 'status'
|
||||||
analyst_readonly_fields = [
|
analyst_readonly_fields = [
|
||||||
"password",
|
|
||||||
"Personal Info",
|
"Personal Info",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
|
@ -727,8 +761,11 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
readonly_fields = list(self.readonly_fields)
|
||||||
|
|
||||||
if request.user.has_perm("registrar.full_access_permission"):
|
if request.user.has_perm("registrar.full_access_permission"):
|
||||||
return () # No read-only fields for all access users
|
return readonly_fields
|
||||||
|
else:
|
||||||
# Return restrictive Read-only fields for analysts and
|
# Return restrictive Read-only fields for analysts and
|
||||||
# users who might not belong to groups
|
# users who might not belong to groups
|
||||||
return self.analyst_readonly_fields
|
return self.analyst_readonly_fields
|
||||||
|
@ -1063,9 +1100,10 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"More details",
|
"Show details",
|
||||||
{
|
{
|
||||||
"classes": ["collapse"],
|
"classes": ["collapse--dotgov"],
|
||||||
|
"description": "Extends type of organization",
|
||||||
"fields": [
|
"fields": [
|
||||||
"federal_type",
|
"federal_type",
|
||||||
# "updated_federal_agency",
|
# "updated_federal_agency",
|
||||||
|
@ -1088,9 +1126,10 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"More details",
|
"Show details",
|
||||||
{
|
{
|
||||||
"classes": ["collapse"],
|
"classes": ["collapse--dotgov"],
|
||||||
|
"description": "Extends organization name and mailing address",
|
||||||
"fields": [
|
"fields": [
|
||||||
"address_line1",
|
"address_line1",
|
||||||
"address_line2",
|
"address_line2",
|
||||||
|
@ -1299,7 +1338,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
||||||
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
(
|
||||||
|
"Contacts",
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
"authorizing_official",
|
||||||
|
"other_contacts",
|
||||||
|
"no_other_contacts_rationale",
|
||||||
|
"cisa_representative_email",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
|
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
|
||||||
(
|
(
|
||||||
"Type of organization",
|
"Type of organization",
|
||||||
|
@ -1312,9 +1361,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"More details",
|
"Show details",
|
||||||
{
|
{
|
||||||
"classes": ["collapse"],
|
"classes": ["collapse--dotgov"],
|
||||||
|
"description": "Extends type of organization",
|
||||||
"fields": [
|
"fields": [
|
||||||
"federal_type",
|
"federal_type",
|
||||||
# "updated_federal_agency",
|
# "updated_federal_agency",
|
||||||
|
@ -1337,9 +1387,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"More details",
|
"Show details",
|
||||||
{
|
{
|
||||||
"classes": ["collapse"],
|
"classes": ["collapse--dotgov"],
|
||||||
|
"description": "Extends organization name and mailing address",
|
||||||
"fields": [
|
"fields": [
|
||||||
"address_line1",
|
"address_line1",
|
||||||
"address_line2",
|
"address_line2",
|
||||||
|
@ -1372,6 +1423,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
"approved_domain",
|
"approved_domain",
|
||||||
|
@ -1827,20 +1879,26 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if domain is not None and hasattr(domain, "domain_info"):
|
if domain is not None and hasattr(domain, "domain_info"):
|
||||||
extra_context["original_object"] = domain.domain_info
|
extra_context["original_object"] = domain.domain_info
|
||||||
|
|
||||||
|
extra_context["state_help_message"] = Domain.State.get_admin_help_text(domain.state)
|
||||||
|
extra_context["domain_state"] = domain.get_state_display()
|
||||||
|
|
||||||
# Pass in what the an extended expiration date would be for the expiration date modal
|
# Pass in what the an extended expiration date would be for the expiration date modal
|
||||||
|
self._set_expiration_date_context(domain, extra_context)
|
||||||
|
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
def _set_expiration_date_context(self, domain, extra_context):
|
||||||
|
"""Given a domain, calculate the an extended expiration date
|
||||||
|
from the current registry expiration date."""
|
||||||
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
|
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
|
||||||
try:
|
try:
|
||||||
curr_exp_date = domain.registry_expiration_date
|
curr_exp_date = domain.registry_expiration_date
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# No expiration date was found. Return none.
|
# No expiration date was found. Return none.
|
||||||
extra_context["extended_expiration_date"] = None
|
extra_context["extended_expiration_date"] = None
|
||||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
else:
|
||||||
new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
|
new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
|
||||||
extra_context["extended_expiration_date"] = new_date
|
extra_context["extended_expiration_date"] = new_date
|
||||||
else:
|
|
||||||
extra_context["extended_expiration_date"] = None
|
|
||||||
|
|
||||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
|
||||||
|
|
||||||
def response_change(self, request, obj):
|
def response_change(self, request, obj):
|
||||||
# Create dictionary of action functions
|
# Create dictionary of action functions
|
||||||
|
|
45
src/registrar/assets/js/dja-collapse.js
Normal file
45
src/registrar/assets/js/dja-collapse.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* We will run our own version of
|
||||||
|
* https://github.com/django/django/blob/195d885ca01b14e3ce9a1881c3b8f7074f953736/django/contrib/admin/static/admin/js/collapse.js
|
||||||
|
* Works with our fieldset override
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*global gettext*/
|
||||||
|
'use strict';
|
||||||
|
{
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
// Add anchor tag for Show/Hide link
|
||||||
|
const fieldsets = document.querySelectorAll('fieldset.collapse--dotgov');
|
||||||
|
for (const [i, elem] of fieldsets.entries()) {
|
||||||
|
// Don't hide if fields in this fieldset have errors
|
||||||
|
if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
|
||||||
|
elem.classList.add('collapsed');
|
||||||
|
const button = elem.querySelector('button');
|
||||||
|
button.id = 'fieldsetcollapser' + i;
|
||||||
|
button.className = 'collapse-toggle--dotgov usa-button usa-button--unstyled';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add toggle to hide/show anchor tag
|
||||||
|
const toggleFuncDotgov = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const fieldset = this.closest('fieldset');
|
||||||
|
const spanElement = this.querySelector('span');
|
||||||
|
const useElement = this.querySelector('use');
|
||||||
|
if (fieldset.classList.contains('collapsed')) {
|
||||||
|
// Show
|
||||||
|
spanElement.textContent = 'Hide details';
|
||||||
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
||||||
|
fieldset.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
// Hide
|
||||||
|
spanElement.textContent = 'Show details';
|
||||||
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
||||||
|
fieldset.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.querySelectorAll('.collapse-toggle--dotgov').forEach(function(el) {
|
||||||
|
el.addEventListener('click', toggleFuncDotgov);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -233,10 +233,8 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
// Initialize custom filter_horizontal widgets; each widget has a "from" select list
|
// Initialize custom filter_horizontal widgets; each widget has a "from" select list
|
||||||
// and a "to" select list; initialization is based off of the presence of the
|
// and a "to" select list; initialization is based off of the presence of the
|
||||||
// "to" select list
|
// "to" select list
|
||||||
checkToListThenInitWidget('id_other_contacts_to', 0);
|
checkToListThenInitWidget('id_groups_to', 0);
|
||||||
checkToListThenInitWidget('id_domain_info-0-other_contacts_to', 0);
|
checkToListThenInitWidget('id_user_permissions_to', 0);
|
||||||
checkToListThenInitWidget('id_current_websites_to', 0);
|
|
||||||
checkToListThenInitWidget('id_alternative_domains_to', 0);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,
|
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,
|
||||||
|
@ -245,219 +243,60 @@ function checkToListThenInitWidget(toListId, attempts) {
|
||||||
let toList = document.getElementById(toListId);
|
let toList = document.getElementById(toListId);
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
if (attempts < 6) {
|
if (attempts < 12) {
|
||||||
if ((toList !== null)) {
|
if (toList) {
|
||||||
// toList found, handle it
|
// toList found, handle it
|
||||||
// Add an event listener on the element
|
// Then get fromList and handle it
|
||||||
// Add disabled buttons on the element's great-grandparent
|
initializeWidgetOnList(toList, ".selector-chosen");
|
||||||
initializeWidgetOnToList(toList, toListId);
|
let fromList = toList.closest('.selector').querySelector(".selector-available select");
|
||||||
|
initializeWidgetOnList(fromList, ".selector-available");
|
||||||
} else {
|
} else {
|
||||||
// Element not found, check again after a delay
|
// Element not found, check again after a delay
|
||||||
setTimeout(() => checkToListThenInitWidget(toListId, attempts), 1000); // Check every 1000 milliseconds (1 second)
|
setTimeout(() => checkToListThenInitWidget(toListId, attempts), 300); // Check every 300 milliseconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the widget:
|
// Initialize the widget:
|
||||||
// add related buttons to the widget for edit, delete and view
|
// Replace h2 with more semantic h3
|
||||||
// add event listeners on the from list, the to list, and selector buttons which either enable or disable the related buttons
|
function initializeWidgetOnList(list, parentId) {
|
||||||
function initializeWidgetOnToList(toList, toListId) {
|
if (list) {
|
||||||
// create the change button
|
// Get h2 and its container
|
||||||
let changeLink = createAndCustomizeLink(
|
const parentElement = list.closest(parentId);
|
||||||
toList,
|
const h2Element = parentElement.querySelector('h2');
|
||||||
toListId,
|
|
||||||
'related-widget-wrapper-link change-related',
|
|
||||||
'Change',
|
|
||||||
'/public/admin/img/icon-changelink.svg',
|
|
||||||
{
|
|
||||||
'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1',
|
|
||||||
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1',
|
|
||||||
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
let hasDeletePermission = hasDeletePermissionOnPage();
|
// One last check
|
||||||
|
if (parentElement && h2Element) {
|
||||||
|
// Create a new <h3> element
|
||||||
|
const h3Element = document.createElement('h3');
|
||||||
|
|
||||||
let deleteLink = null;
|
// Copy the text content from the <h2> element to the <h3> element
|
||||||
if (hasDeletePermission) {
|
h3Element.textContent = h2Element.textContent;
|
||||||
// create the delete button if user has permission to delete
|
|
||||||
deleteLink = createAndCustomizeLink(
|
// Find the nested <span> element inside the <h2>
|
||||||
toList,
|
const nestedSpan = h2Element.querySelector('span[class][title]');
|
||||||
toListId,
|
|
||||||
'related-widget-wrapper-link delete-related',
|
// If the nested <span> element exists
|
||||||
'Delete',
|
if (nestedSpan) {
|
||||||
'/public/admin/img/icon-deletelink.svg',
|
// Create a new <span> element
|
||||||
{
|
const newSpan = document.createElement('span');
|
||||||
'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1',
|
|
||||||
'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
|
// Copy the class and title attributes from the nested <span> element
|
||||||
'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
|
newSpan.className = nestedSpan.className;
|
||||||
},
|
newSpan.title = nestedSpan.title;
|
||||||
true,
|
|
||||||
false
|
// Append the new <span> element to the <h3> element
|
||||||
);
|
h3Element.appendChild(newSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the view button
|
// Replace the <h2> element with the new <h3> element
|
||||||
let viewLink = createAndCustomizeLink(
|
parentElement.replaceChild(h3Element, h2Element);
|
||||||
toList,
|
|
||||||
toListId,
|
|
||||||
'related-widget-wrapper-link view-related',
|
|
||||||
'View',
|
|
||||||
'/public/admin/img/icon-viewlink.svg',
|
|
||||||
{
|
|
||||||
'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id',
|
|
||||||
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
|
||||||
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
|
||||||
},
|
|
||||||
// NOTE: If we open view in the same window then use the back button
|
|
||||||
// to go back, the 'chosen' list will fail to initialize correctly in
|
|
||||||
// sandbozes (but will work fine on local). This is related to how the
|
|
||||||
// Django JS runs (SelectBox.js) and is probably due to a race condition.
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
// identify the fromList element in the DOM
|
|
||||||
let fromList = toList.closest('.selector').querySelector(".selector-available select");
|
|
||||||
|
|
||||||
fromList.addEventListener('click', function(event) {
|
|
||||||
handleSelectClick(fromList, changeLink, deleteLink, viewLink);
|
|
||||||
});
|
|
||||||
|
|
||||||
toList.addEventListener('click', function(event) {
|
|
||||||
handleSelectClick(toList, changeLink, deleteLink, viewLink);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable buttons when the selectors are interacted with (items are moved from one column to the other)
|
|
||||||
let selectorButtons = [];
|
|
||||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-chooseall"));
|
|
||||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-add"));
|
|
||||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-remove"));
|
|
||||||
|
|
||||||
selectorButtons.forEach((selector) => {
|
|
||||||
selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and customize the button, then add to the DOM, relative to the toList
|
|
||||||
// toList - the element in the DOM for the toList
|
|
||||||
// toListId - the ID of the element in the DOM
|
|
||||||
// className - className to add to the created link
|
|
||||||
// action - the action to perform on the item {change, delete, view}
|
|
||||||
// imgSrc - the img.src for the created link
|
|
||||||
// dataMappings - dictionary which relates toListId to href for the created link
|
|
||||||
// dataPopup - boolean for whether the link should produce a popup window
|
|
||||||
// firstPosition - boolean indicating if link should be first position in list of links, otherwise, should be last link
|
|
||||||
function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, firstPosition) {
|
|
||||||
// Create a link element
|
|
||||||
var link = document.createElement('a');
|
|
||||||
|
|
||||||
// Set class attribute for the link
|
|
||||||
link.className = className;
|
|
||||||
|
|
||||||
// Set id
|
|
||||||
// Determine function {change, link, view} from the className
|
|
||||||
// Add {function}_ to the beginning of the string
|
|
||||||
let modifiedLinkString = className.split('-')[0] + '_' + toListId;
|
|
||||||
// Remove '_to' from the end of the string
|
|
||||||
modifiedLinkString = modifiedLinkString.replace('_to', '');
|
|
||||||
link.id = modifiedLinkString;
|
|
||||||
|
|
||||||
// Set data-href-template
|
|
||||||
for (const [idPattern, template] of Object.entries(dataMappings)) {
|
|
||||||
if (toListId.includes(idPattern)) {
|
|
||||||
link.setAttribute('data-href-template', template);
|
|
||||||
break; // Stop checking once a match is found
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataPopup)
|
|
||||||
link.setAttribute('data-popup', 'yes');
|
|
||||||
|
|
||||||
link.setAttribute('title-template', action + " selected item")
|
|
||||||
link.title = link.getAttribute('title-template');
|
|
||||||
|
|
||||||
// Create an 'img' element
|
|
||||||
var img = document.createElement('img');
|
|
||||||
|
|
||||||
// Set attributes for the new image
|
|
||||||
img.src = imgSrc;
|
|
||||||
img.alt = action;
|
|
||||||
|
|
||||||
// Append the image to the link
|
|
||||||
link.appendChild(img);
|
|
||||||
|
|
||||||
let relatedWidgetWrapper = toList.closest('.related-widget-wrapper');
|
|
||||||
// If firstPosition is true, insert link as the first child element
|
|
||||||
if (firstPosition) {
|
|
||||||
relatedWidgetWrapper.insertBefore(link, relatedWidgetWrapper.children[0]);
|
|
||||||
} else {
|
|
||||||
// otherwise, insert the link prior to the last child (which is a div)
|
|
||||||
// and also prior to any text elements immediately preceding the last
|
|
||||||
// child node
|
|
||||||
var lastChild = relatedWidgetWrapper.lastChild;
|
|
||||||
|
|
||||||
// Check if lastChild is an element node (not a text node, comment, etc.)
|
|
||||||
if (lastChild.nodeType === 1) {
|
|
||||||
var previousSibling = lastChild.previousSibling;
|
|
||||||
// need to work around some white space which has been inserted into the dom
|
|
||||||
while (previousSibling.nodeType !== 1) {
|
|
||||||
previousSibling = previousSibling.previousSibling;
|
|
||||||
}
|
|
||||||
relatedWidgetWrapper.insertBefore(link, previousSibling.nextSibling);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the link, which we'll use in the disable and enable functions
|
|
||||||
return link;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either enable or disable widget buttons when select is clicked. Action (enable or disable) taken depends on the count
|
|
||||||
// of selected items in selectElement. If exactly one item is selected, buttons are enabled, and urls for the buttons are
|
|
||||||
// associated with the selected item
|
|
||||||
function handleSelectClick(selectElement, changeLink, deleteLink, viewLink) {
|
|
||||||
|
|
||||||
// If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them
|
|
||||||
if (selectElement.selectedOptions.length === 1) {
|
|
||||||
// enable buttons for selected item in selectElement
|
|
||||||
enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value, selectElement.selectedOptions[0].text);
|
|
||||||
} else {
|
|
||||||
disableRelatedWidgetButtons(changeLink, deleteLink, viewLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return true if there exist elements on the page with classname of delete-related.
|
|
||||||
// presence of one or more of these elements indicates user has permission to delete
|
|
||||||
function hasDeletePermissionOnPage() {
|
|
||||||
return document.querySelector('.delete-related') != null
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) {
|
|
||||||
changeLink.removeAttribute('href');
|
|
||||||
changeLink.setAttribute('title', changeLink.getAttribute('title-template'));
|
|
||||||
if (deleteLink) {
|
|
||||||
deleteLink.removeAttribute('href');
|
|
||||||
deleteLink.setAttribute('title', deleteLink.getAttribute('title-template'));
|
|
||||||
}
|
|
||||||
viewLink.removeAttribute('href');
|
|
||||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, elementText) {
|
|
||||||
changeLink.setAttribute('href', changeLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
|
||||||
changeLink.setAttribute('title', changeLink.getAttribute('title-template').replace('selected item', elementText));
|
|
||||||
if (deleteLink) {
|
|
||||||
deleteLink.setAttribute('href', deleteLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
|
||||||
deleteLink.setAttribute('title', deleteLink.getAttribute('title-template').replace('selected item', elementText));
|
|
||||||
}
|
|
||||||
viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
|
||||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
||||||
* status select amd to show/hide the rejection reason
|
* status select and to show/hide the rejection reason
|
||||||
*/
|
*/
|
||||||
(function (){
|
(function (){
|
||||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
||||||
|
|
|
@ -193,6 +193,65 @@ function clearValidators(el) {
|
||||||
toggleInputValidity(el, true);
|
toggleInputValidity(el, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Hookup listeners for yes/no togglers for form fields
|
||||||
|
* Parameters:
|
||||||
|
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
|
||||||
|
* - elementIdToShowIfYes: The Id of the element (eg. a div) to show if selected value of the given
|
||||||
|
* radio button is true (hides this element if false)
|
||||||
|
* - elementIdToShowIfNo: The Id of the element (eg. a div) to show if selected value of the given
|
||||||
|
* radio button is false (hides this element if true)
|
||||||
|
* **/
|
||||||
|
function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) {
|
||||||
|
// Get the radio buttons
|
||||||
|
let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]');
|
||||||
|
|
||||||
|
function handleRadioButtonChange() {
|
||||||
|
// Check the value of the selected radio button
|
||||||
|
// Attempt to find the radio button element that is checked
|
||||||
|
let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked');
|
||||||
|
|
||||||
|
// Check if the element exists before accessing its value
|
||||||
|
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
||||||
|
|
||||||
|
switch (selectedValue) {
|
||||||
|
case 'True':
|
||||||
|
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'False':
|
||||||
|
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (radioButtons.length) {
|
||||||
|
// Add event listener to each radio button
|
||||||
|
radioButtons.forEach(function (radioButton) {
|
||||||
|
radioButton.addEventListener('change', handleRadioButtonChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
handleRadioButtonChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
|
||||||
|
function toggleTwoDomElements(ele1, ele2, index) {
|
||||||
|
let element1 = document.getElementById(ele1);
|
||||||
|
let element2 = document.getElementById(ele2);
|
||||||
|
if (element1 || element2) {
|
||||||
|
// Toggle display based on the index
|
||||||
|
if (element1) {element1.style.display = index === 1 ? 'block' : 'none';}
|
||||||
|
if (element2) {element2.style.display = index === 2 ? 'block' : 'none';}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error('Unable to find elements to toggle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
// Event handlers.
|
// Event handlers.
|
||||||
|
|
||||||
|
@ -712,57 +771,40 @@ function hideDeletedForms() {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
|
|
||||||
function toggleTwoDomElements(ele1, ele2, index) {
|
|
||||||
let element1 = document.getElementById(ele1);
|
|
||||||
let element2 = document.getElementById(ele2);
|
|
||||||
if (element1 && element2) {
|
|
||||||
// Toggle display based on the index
|
|
||||||
element1.style.display = index === 1 ? 'block' : 'none';
|
|
||||||
element2.style.display = index === 2 ? 'block' : 'none';
|
|
||||||
} else {
|
|
||||||
console.error('One or both elements not found.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
|
* An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
(function otherContactsFormListener() {
|
(function otherContactsFormListener() {
|
||||||
// Get the radio buttons
|
HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees')
|
||||||
let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]');
|
})();
|
||||||
|
|
||||||
function handleRadioButtonChange() {
|
|
||||||
// Check the value of the selected radio button
|
|
||||||
// Attempt to find the radio button element that is checked
|
|
||||||
let radioButtonChecked = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked');
|
|
||||||
|
|
||||||
// Check if the element exists before accessing its value
|
/**
|
||||||
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
* An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
(function anythingElseFormListener() {
|
||||||
|
HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null)
|
||||||
|
})();
|
||||||
|
|
||||||
switch (selectedValue) {
|
/**
|
||||||
case 'True':
|
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
|
||||||
toggleTwoDomElements('other-employees', 'no-other-employees', 1);
|
*
|
||||||
break;
|
*/
|
||||||
|
(function nameserversFormListener() {
|
||||||
case 'False':
|
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||||
toggleTwoDomElements('other-employees', 'no-other-employees', 2);
|
if (isNameserversForm) {
|
||||||
break;
|
let forms = document.querySelectorAll(".repeatable-form");
|
||||||
|
if (forms.length < 3) {
|
||||||
default:
|
// Hide the delete buttons on the 2 nameservers
|
||||||
toggleTwoDomElements('other-employees', 'no-other-employees', 0);
|
forms.forEach((form) => {
|
||||||
}
|
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||||
}
|
deleteButton.setAttribute("disabled", "true");
|
||||||
|
|
||||||
if (radioButtons.length) {
|
|
||||||
// Add event listener to each radio button
|
|
||||||
radioButtons.forEach(function (radioButton) {
|
|
||||||
radioButton.addEventListener('change', handleRadioButtonChange);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// initialize
|
}
|
||||||
handleRadioButtonChange();
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -784,3 +826,11 @@ function toggleTwoDomElements(ele1, ele2, index) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An IIFE that listens to the yes/no radio buttons on the CISA representatives form and toggles form field visibility accordingly
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
(function cisaRepresentativesFormListener() {
|
||||||
|
HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null)
|
||||||
|
})();
|
||||||
|
|
|
@ -112,12 +112,20 @@ html[data-theme="light"] {
|
||||||
.change-list .usa-table--borderless thead th,
|
.change-list .usa-table--borderless thead th,
|
||||||
.change-list .usa-table thead td,
|
.change-list .usa-table thead td,
|
||||||
.change-list .usa-table thead th,
|
.change-list .usa-table thead th,
|
||||||
|
.change-form .usa-table,
|
||||||
|
.change-form .usa-table--striped tbody tr:nth-child(odd) td,
|
||||||
|
.change-form .usa-table--borderless thead th,
|
||||||
|
.change-form .usa-table thead td,
|
||||||
|
.change-form .usa-table thead th,
|
||||||
body.dashboard,
|
body.dashboard,
|
||||||
body.change-list,
|
body.change-list,
|
||||||
body.change-form,
|
body.change-form,
|
||||||
.analytics {
|
.analytics {
|
||||||
color: var(--body-fg);
|
color: var(--body-fg);
|
||||||
}
|
}
|
||||||
|
.usa-table td {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Firefox needs this to be specifically set
|
// Firefox needs this to be specifically set
|
||||||
|
@ -127,11 +135,20 @@ html[data-theme="dark"] {
|
||||||
.change-list .usa-table--borderless thead th,
|
.change-list .usa-table--borderless thead th,
|
||||||
.change-list .usa-table thead td,
|
.change-list .usa-table thead td,
|
||||||
.change-list .usa-table thead th,
|
.change-list .usa-table thead th,
|
||||||
|
.change-form .usa-table,
|
||||||
|
.change-form .usa-table--striped tbody tr:nth-child(odd) td,
|
||||||
|
.change-form .usa-table--borderless thead th,
|
||||||
|
.change-form .usa-table thead td,
|
||||||
|
.change-form .usa-table thead th,
|
||||||
body.dashboard,
|
body.dashboard,
|
||||||
body.change-list,
|
body.change-list,
|
||||||
body.change-form {
|
body.change-form,
|
||||||
|
.analytics {
|
||||||
color: var(--body-fg);
|
color: var(--body-fg);
|
||||||
}
|
}
|
||||||
|
.usa-table td {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#branding h1 a:link, #branding h1 a:visited {
|
#branding h1 a:link, #branding h1 a:visited {
|
||||||
|
@ -525,17 +542,30 @@ address.dja-address-contact-list {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapse button styles for fieldsets
|
// Collapse button styles for fieldsets
|
||||||
.module.collapse {
|
.module.collapse--dotgov {
|
||||||
margin-top: -35px;
|
margin-top: -35px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
border: none;
|
border: none;
|
||||||
h2 {
|
button {
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--body-fg)!important;
|
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--link-fg);
|
color: var(--link-fg);
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: 10px;
|
||||||
|
span {
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: 13px;
|
||||||
|
font-feature-settings: "kern";
|
||||||
|
font-kerning: normal;
|
||||||
|
line-height: 13px;
|
||||||
|
font-family: -apple-system, "system-ui", "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collapse--dotgov.collapsed .collapse-toggle--dotgov {
|
||||||
|
display: inline-block!important;
|
||||||
|
* {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -617,3 +647,32 @@ address.dja-address-contact-list {
|
||||||
.usa-button__small-text {
|
.usa-button__small-text {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get rid of padding on all help texts
|
||||||
|
form .aligned p.help, form .aligned div.help {
|
||||||
|
padding-left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We override the DJA header on multi list selects from h2 to h3
|
||||||
|
// The following block of code styles our generated h3s to match the old h2s
|
||||||
|
.selector .selector-available h3 {
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-available h3, .selector-chosen h3 {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector .selector-chosen h3 {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--header-link-color);
|
||||||
|
}
|
||||||
|
|
21
src/registrar/assets/sass/_theme/_links.scss
Normal file
21
src/registrar/assets/sass/_theme/_links.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
|
.dotgov-table {
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
color: color('primary');
|
||||||
|
|
||||||
|
&:visited {
|
||||||
|
color: color('primary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
.usa-icon {
|
||||||
|
// align icon with x height
|
||||||
|
margin-top: units(0.5);
|
||||||
|
margin-right: units(0.5);
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,22 +56,6 @@
|
||||||
.dotgov-table {
|
.dotgov-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
color: color('primary');
|
|
||||||
|
|
||||||
&:visited {
|
|
||||||
color: color('primary');
|
|
||||||
}
|
|
||||||
|
|
||||||
.usa-icon {
|
|
||||||
// align icon with x height
|
|
||||||
margin-top: units(0.5);
|
|
||||||
margin-right: units(0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
|
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
|
||||||
right: auto;
|
right: auto;
|
||||||
}
|
}
|
||||||
|
@ -108,12 +92,51 @@
|
||||||
padding: units(2) units(2) units(2) 0;
|
padding: units(2) units(2) units(2) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:first-of-type {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead tr:first-child th:first-child {
|
thead tr:first-child th:first-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (min-width: 1040px){
|
||||||
|
.dotgov-table__domain-requests {
|
||||||
|
th:nth-of-type(1) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(2) {
|
||||||
|
width: 158px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(3) {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(4) {
|
||||||
|
width: 95px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(5) {
|
||||||
|
width: 85px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1040px){
|
||||||
|
.dotgov-table__registered-domains {
|
||||||
|
th:nth-of-type(1) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(2) {
|
||||||
|
width: 158px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(3) {
|
||||||
|
width: 215px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-of-type(4) {
|
||||||
|
width: 95px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
--- Custom Styles ---------------------------------*/
|
--- Custom Styles ---------------------------------*/
|
||||||
@forward "base";
|
@forward "base";
|
||||||
@forward "typography";
|
@forward "typography";
|
||||||
|
@forward "links";
|
||||||
@forward "lists";
|
@forward "lists";
|
||||||
@forward "buttons";
|
@forward "buttons";
|
||||||
@forward "forms";
|
@forward "forms";
|
||||||
|
|
|
@ -782,3 +782,11 @@ if DEBUG:
|
||||||
# due to Docker, bypass Debug Toolbar's check on INTERNAL_IPS
|
# due to Docker, bypass Debug Toolbar's check on INTERNAL_IPS
|
||||||
"SHOW_TOOLBAR_CALLBACK": lambda _: True,
|
"SHOW_TOOLBAR_CALLBACK": lambda _: True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# From https://django-auditlog.readthedocs.io/en/latest/upgrade.html
|
||||||
|
# Run:
|
||||||
|
# cf run-task getgov-<> --wait --command 'python manage.py auditlogmigratejson --traceback' --name auditlogmigratejson
|
||||||
|
# on our staging and stable, then remove these 2 variables or set to False
|
||||||
|
AUDITLOG_TWO_STEP_MIGRATION = True
|
||||||
|
|
||||||
|
AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = True
|
||||||
|
|
|
@ -46,7 +46,7 @@ for step, view in [
|
||||||
(Step.PURPOSE, views.Purpose),
|
(Step.PURPOSE, views.Purpose),
|
||||||
(Step.YOUR_CONTACT, views.YourContact),
|
(Step.YOUR_CONTACT, views.YourContact),
|
||||||
(Step.OTHER_CONTACTS, views.OtherContacts),
|
(Step.OTHER_CONTACTS, views.OtherContacts),
|
||||||
(Step.ANYTHING_ELSE, views.AnythingElse),
|
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
||||||
(Step.REQUIREMENTS, views.Requirements),
|
(Step.REQUIREMENTS, views.Requirements),
|
||||||
(Step.REVIEW, views.Review),
|
(Step.REVIEW, views.Review),
|
||||||
]:
|
]:
|
||||||
|
|
|
@ -7,6 +7,7 @@ from registrar.models import (
|
||||||
UserGroup,
|
UserGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
fake = Faker()
|
fake = Faker()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -93,6 +94,12 @@ class UserFixture:
|
||||||
"last_name": "Chin",
|
"last_name": "Chin",
|
||||||
"email": "szu.chin@associates.cisa.dhs.gov",
|
"email": "szu.chin@associates.cisa.dhs.gov",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "66bb1a5a-a091-4d7f-a6cf-4d772b4711c7",
|
||||||
|
"first_name": "Christina",
|
||||||
|
"last_name": "Burnett",
|
||||||
|
"email": "christina.burnett@cisa.dhs.gov",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e",
|
"username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e",
|
||||||
"first_name": "Riley",
|
"first_name": "Riley",
|
||||||
|
@ -169,6 +176,12 @@ class UserFixture:
|
||||||
"last_name": "Chin-Analyst",
|
"last_name": "Chin-Analyst",
|
||||||
"email": "szu.chin@ecstech.com",
|
"email": "szu.chin@ecstech.com",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "22f88aa5-3b54-4b1f-9c57-201fb02ddba7",
|
||||||
|
"first_name": "Christina-Analyst",
|
||||||
|
"last_name": "Burnett-Analyst",
|
||||||
|
"email": "christina.burnett@gwe.cisa.dhs.gov",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
|
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
|
||||||
"first_name": "Alex-Analyst",
|
"first_name": "Alex-Analyst",
|
||||||
|
@ -195,6 +208,10 @@ class UserFixture:
|
||||||
user.email = user_data["email"]
|
user.email = user_data["email"]
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.is_active = True
|
user.is_active = True
|
||||||
|
# This verification type will get reverted to "regular" (or whichever is applicables)
|
||||||
|
# once the user logs in for the first time (as they then got verified through different means).
|
||||||
|
# In the meantime, we can still describe how the user got here in the first place.
|
||||||
|
user.verification_type = User.VerificationTypeChoices.FIXTURE_USER
|
||||||
group = UserGroup.objects.get(name=group_name)
|
group = UserGroup.objects.get(name=group_name)
|
||||||
user.groups.add(group)
|
user.groups.add(group)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
from __future__ import annotations # allows forward references in annotations
|
from __future__ import annotations # allows forward references in annotations
|
||||||
from itertools import zip_longest
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
|
||||||
from api.views import DOMAIN_API_MESSAGES
|
from api.views import DOMAIN_API_MESSAGES
|
||||||
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.db.models.fields.related import ForeignObjectRel
|
|
||||||
|
|
||||||
|
from registrar.forms.utility.wizard_form_helper import (
|
||||||
|
RegistrarForm,
|
||||||
|
RegistrarFormSet,
|
||||||
|
BaseYesNoForm,
|
||||||
|
BaseDeletableRegistrarForm,
|
||||||
|
)
|
||||||
from registrar.models import Contact, DomainRequest, DraftDomain, Domain
|
from registrar.models import Contact, DomainRequest, DraftDomain, Domain
|
||||||
from registrar.templatetags.url_helpers import public_site_url
|
from registrar.templatetags.url_helpers import public_site_url
|
||||||
from registrar.utility.enums import ValidationReturnType
|
from registrar.utility.enums import ValidationReturnType
|
||||||
|
@ -17,157 +20,6 @@ from registrar.utility.enums import ValidationReturnType
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RegistrarForm(forms.Form):
|
|
||||||
"""
|
|
||||||
A common set of methods and configuration.
|
|
||||||
|
|
||||||
The registrar's domain request is several pages of "steps".
|
|
||||||
Each step is an HTML form containing one or more Django "forms".
|
|
||||||
|
|
||||||
Subclass this class to create new forms.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs.setdefault("label_suffix", "")
|
|
||||||
# save a reference to a domain request object
|
|
||||||
self.domain_request = kwargs.pop("domain_request", None)
|
|
||||||
super(RegistrarForm, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_database(self, obj: DomainRequest | Contact):
|
|
||||||
"""
|
|
||||||
Adds this form's cleaned data to `obj` and saves `obj`.
|
|
||||||
|
|
||||||
Does nothing if form is not valid.
|
|
||||||
"""
|
|
||||||
if not self.is_valid():
|
|
||||||
return
|
|
||||||
for name, value in self.cleaned_data.items():
|
|
||||||
setattr(obj, name, value)
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_database(cls, obj: DomainRequest | Contact | None):
|
|
||||||
"""Returns a dict of form field values gotten from `obj`."""
|
|
||||||
if obj is None:
|
|
||||||
return {}
|
|
||||||
return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrarFormSet(forms.BaseFormSet):
|
|
||||||
"""
|
|
||||||
As with RegistrarForm, a common set of methods and configuration.
|
|
||||||
|
|
||||||
Subclass this class to create new formsets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# save a reference to an domain_request object
|
|
||||||
self.domain_request = kwargs.pop("domain_request", None)
|
|
||||||
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
|
||||||
# quick workaround to ensure that the HTML `required`
|
|
||||||
# attribute shows up on required fields for any forms
|
|
||||||
# in the formset which have data already (stated another
|
|
||||||
# way: you can leave a form in the formset blank, but
|
|
||||||
# if you opt to fill it out, you must fill it out _right_)
|
|
||||||
for index in range(self.initial_form_count()):
|
|
||||||
self.forms[index].use_required_attribute = True
|
|
||||||
|
|
||||||
def should_delete(self, cleaned):
|
|
||||||
"""Should this entry be deleted from the database?"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def pre_update(self, db_obj, cleaned):
|
|
||||||
"""Code to run before an item in the formset is saved."""
|
|
||||||
for key, value in cleaned.items():
|
|
||||||
setattr(db_obj, key, value)
|
|
||||||
|
|
||||||
def pre_create(self, db_obj, cleaned):
|
|
||||||
"""Code to run before an item in the formset is created in the database."""
|
|
||||||
return cleaned
|
|
||||||
|
|
||||||
def to_database(self, obj: DomainRequest):
|
|
||||||
"""
|
|
||||||
Adds this form's cleaned data to `obj` and saves `obj`.
|
|
||||||
|
|
||||||
Does nothing if form is not valid.
|
|
||||||
|
|
||||||
Hint: Subclass should call `self._to_database(...)`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _to_database(
|
|
||||||
self,
|
|
||||||
obj: DomainRequest,
|
|
||||||
join: str,
|
|
||||||
should_delete: Callable,
|
|
||||||
pre_update: Callable,
|
|
||||||
pre_create: Callable,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Performs the actual work of saving.
|
|
||||||
|
|
||||||
Has hooks such as `should_delete` and `pre_update` by which the
|
|
||||||
subclass can control behavior. Add more hooks whenever needed.
|
|
||||||
"""
|
|
||||||
if not self.is_valid():
|
|
||||||
return
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
query = getattr(obj, join).order_by("created_at").all() # order matters
|
|
||||||
|
|
||||||
# get the related name for the join defined for the db_obj for this form.
|
|
||||||
# the related name will be the reference on a related object back to db_obj
|
|
||||||
related_name = ""
|
|
||||||
field = obj._meta.get_field(join)
|
|
||||||
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
|
|
||||||
related_name = field.related_query_name()
|
|
||||||
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
|
|
||||||
related_name = field.related_query_name()
|
|
||||||
|
|
||||||
# the use of `zip` pairs the forms in the formset with the
|
|
||||||
# related objects gotten from the database -- there should always be
|
|
||||||
# at least as many forms as database entries: extra forms means new
|
|
||||||
# entries, but fewer forms is _not_ the correct way to delete items
|
|
||||||
# (likely a client-side error or an attempt at data tampering)
|
|
||||||
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
|
||||||
cleaned = post_data.cleaned_data if post_data is not None else {}
|
|
||||||
|
|
||||||
# matching database object exists, update it
|
|
||||||
if db_obj is not None and cleaned:
|
|
||||||
if should_delete(cleaned):
|
|
||||||
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
|
||||||
# Remove the specific relationship without deleting the object
|
|
||||||
getattr(db_obj, related_name).remove(self.domain_request)
|
|
||||||
else:
|
|
||||||
# If there are no other relationships, delete the object
|
|
||||||
db_obj.delete()
|
|
||||||
else:
|
|
||||||
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
|
||||||
# create a new db_obj and disconnect existing one
|
|
||||||
getattr(db_obj, related_name).remove(self.domain_request)
|
|
||||||
kwargs = pre_create(db_obj, cleaned)
|
|
||||||
getattr(obj, join).create(**kwargs)
|
|
||||||
else:
|
|
||||||
pre_update(db_obj, cleaned)
|
|
||||||
db_obj.save()
|
|
||||||
|
|
||||||
# no matching database object, create it
|
|
||||||
# make sure not to create a database object if cleaned has 'delete' attribute
|
|
||||||
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
|
|
||||||
kwargs = pre_create(db_obj, cleaned)
|
|
||||||
getattr(obj, join).create(**kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def on_fetch(cls, query):
|
|
||||||
"""Code to run when fetching formset's objects from the database."""
|
|
||||||
return query.values()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable):
|
|
||||||
"""Returns a dict of form field values gotten from `obj`."""
|
|
||||||
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationTypeForm(RegistrarForm):
|
class OrganizationTypeForm(RegistrarForm):
|
||||||
generic_org_type = forms.ChoiceField(
|
generic_org_type = forms.ChoiceField(
|
||||||
# use the long names in the domain request form
|
# use the long names in the domain request form
|
||||||
|
@ -588,28 +440,24 @@ class YourContactForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OtherContactsYesNoForm(RegistrarForm):
|
class OtherContactsYesNoForm(BaseYesNoForm):
|
||||||
def __init__(self, *args, **kwargs):
|
"""The yes/no field for the OtherContacts form."""
|
||||||
"""Extend the initialization of the form from RegistrarForm __init__"""
|
|
||||||
super().__init__(*args, **kwargs)
|
form_choices = ((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)"))
|
||||||
# set the initial value based on attributes of domain request
|
field_name = "has_other_contacts"
|
||||||
if self.domain_request and self.domain_request.has_other_contacts():
|
|
||||||
initial_value = True
|
@property
|
||||||
elif self.domain_request and self.domain_request.has_rationale():
|
def form_is_checked(self):
|
||||||
initial_value = False
|
"""
|
||||||
|
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||||
|
"""
|
||||||
|
if self.domain_request.has_other_contacts():
|
||||||
|
return True
|
||||||
|
elif self.domain_request.has_rationale():
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
# No pre-selection for new domain requests
|
# No pre-selection for new domain requests
|
||||||
initial_value = None
|
return None
|
||||||
|
|
||||||
self.fields["has_other_contacts"] = forms.TypedChoiceField(
|
|
||||||
coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None
|
|
||||||
choices=((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)")),
|
|
||||||
initial=initial_value,
|
|
||||||
widget=forms.RadioSelect,
|
|
||||||
error_messages={
|
|
||||||
"required": "This question is required.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OtherContactsForm(RegistrarForm):
|
class OtherContactsForm(RegistrarForm):
|
||||||
|
@ -779,7 +627,7 @@ OtherContactsFormSet = forms.formset_factory(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NoOtherContactsForm(RegistrarForm):
|
class NoOtherContactsForm(BaseDeletableRegistrarForm):
|
||||||
no_other_contacts_rationale = forms.CharField(
|
no_other_contacts_rationale = forms.CharField(
|
||||||
required=True,
|
required=True,
|
||||||
# label has to end in a space to get the label_suffix to show
|
# label has to end in a space to get the label_suffix to show
|
||||||
|
@ -794,59 +642,35 @@ class NoOtherContactsForm(RegistrarForm):
|
||||||
error_messages={"required": ("Rationale for no other employees is required.")},
|
error_messages={"required": ("Rationale for no other employees is required.")},
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.form_data_marked_for_deletion = False
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def mark_form_for_deletion(self):
|
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
|
||||||
"""Marks no_other_contacts form for deletion.
|
cisa_representative_email = forms.EmailField(
|
||||||
This changes behavior of validity checks and to_database
|
required=True,
|
||||||
methods."""
|
max_length=None,
|
||||||
self.form_data_marked_for_deletion = True
|
label="Your representative’s email",
|
||||||
|
validators=[
|
||||||
def clean(self):
|
MaxLengthValidator(
|
||||||
"""
|
320,
|
||||||
This method overrides the default behavior for forms.
|
message="Response must be less than 320 characters.",
|
||||||
This cleans the form after field validation has already taken place.
|
)
|
||||||
In this override, remove errors associated with the form if form data
|
],
|
||||||
is marked for deletion.
|
error_messages={
|
||||||
"""
|
"invalid": ("Enter your email address in the required format, like name@example.com."),
|
||||||
|
"required": ("Enter the email address of your CISA regional representative."),
|
||||||
if self.form_data_marked_for_deletion:
|
},
|
||||||
# clear any errors raised by the form fields
|
)
|
||||||
# (before this clean() method is run, each field
|
|
||||||
# performs its own clean, which could result in
|
|
||||||
# errors that we wish to ignore at this point)
|
|
||||||
#
|
|
||||||
# NOTE: we cannot just clear() the errors list.
|
|
||||||
# That causes problems.
|
|
||||||
for field in self.fields:
|
|
||||||
if field in self.errors:
|
|
||||||
del self.errors[field]
|
|
||||||
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
def to_database(self, obj):
|
|
||||||
"""
|
|
||||||
This method overrides the behavior of RegistrarForm.
|
|
||||||
If form data is marked for deletion, set relevant fields
|
|
||||||
to None before saving.
|
|
||||||
Do nothing if form is not valid.
|
|
||||||
"""
|
|
||||||
if not self.is_valid():
|
|
||||||
return
|
|
||||||
if self.form_data_marked_for_deletion:
|
|
||||||
for field_name, _ in self.fields.items():
|
|
||||||
setattr(obj, field_name, None)
|
|
||||||
else:
|
|
||||||
for name, value in self.cleaned_data.items():
|
|
||||||
setattr(obj, name, value)
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
|
|
||||||
class AnythingElseForm(RegistrarForm):
|
class CisaRepresentativeYesNoForm(BaseYesNoForm):
|
||||||
|
"""Yes/no toggle for the CISA regions question on additional details"""
|
||||||
|
|
||||||
|
form_is_checked = property(lambda self: self.domain_request.has_cisa_representative) # type: ignore
|
||||||
|
field_name = "has_cisa_representative"
|
||||||
|
|
||||||
|
|
||||||
|
class AdditionalDetailsForm(BaseDeletableRegistrarForm):
|
||||||
anything_else = forms.CharField(
|
anything_else = forms.CharField(
|
||||||
required=False,
|
required=True,
|
||||||
label="Anything else?",
|
label="Anything else?",
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(),
|
||||||
validators=[
|
validators=[
|
||||||
|
@ -855,7 +679,20 @@ class AnythingElseForm(RegistrarForm):
|
||||||
message="Response must be less than 2000 characters.",
|
message="Response must be less than 2000 characters.",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
error_messages={
|
||||||
|
"required": (
|
||||||
|
"Provide additional details you’d like us to know. " "If you have nothing to add, select “No.”"
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdditionalDetailsYesNoForm(BaseYesNoForm):
|
||||||
|
"""Yes/no toggle for the anything else question on additional details"""
|
||||||
|
|
||||||
|
# Note that these can be set as functions/init if you need more fine-grained control.
|
||||||
|
form_is_checked = property(lambda self: self.domain_request.has_anything_else_text) # type: ignore
|
||||||
|
field_name = "has_anything_else_text"
|
||||||
|
|
||||||
|
|
||||||
class RequirementsForm(RegistrarForm):
|
class RequirementsForm(RegistrarForm):
|
||||||
|
|
280
src/registrar/forms/utility/wizard_form_helper.py
Normal file
280
src/registrar/forms/utility/wizard_form_helper.py
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
"""Containers helpers and base classes for the domain_request_wizard.py file"""
|
||||||
|
|
||||||
|
from itertools import zip_longest
|
||||||
|
from typing import Callable
|
||||||
|
from django.db.models.fields.related import ForeignObjectRel
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from registrar.models import DomainRequest, Contact
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrarForm(forms.Form):
|
||||||
|
"""
|
||||||
|
A common set of methods and configuration.
|
||||||
|
|
||||||
|
The registrar's domain request is several pages of "steps".
|
||||||
|
Each step is an HTML form containing one or more Django "forms".
|
||||||
|
|
||||||
|
Subclass this class to create new forms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault("label_suffix", "")
|
||||||
|
# save a reference to a domain request object
|
||||||
|
self.domain_request = kwargs.pop("domain_request", None)
|
||||||
|
super(RegistrarForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_database(self, obj: DomainRequest | Contact):
|
||||||
|
"""
|
||||||
|
Adds this form's cleaned data to `obj` and saves `obj`.
|
||||||
|
|
||||||
|
Does nothing if form is not valid.
|
||||||
|
"""
|
||||||
|
if not self.is_valid():
|
||||||
|
return
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
setattr(obj, name, value)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainRequest | Contact | None):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`."""
|
||||||
|
if obj is None:
|
||||||
|
return {}
|
||||||
|
return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrarFormSet(forms.BaseFormSet):
|
||||||
|
"""
|
||||||
|
As with RegistrarForm, a common set of methods and configuration.
|
||||||
|
|
||||||
|
Subclass this class to create new formsets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# save a reference to an domain_request object
|
||||||
|
self.domain_request = kwargs.pop("domain_request", None)
|
||||||
|
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
||||||
|
# quick workaround to ensure that the HTML `required`
|
||||||
|
# attribute shows up on required fields for any forms
|
||||||
|
# in the formset which have data already (stated another
|
||||||
|
# way: you can leave a form in the formset blank, but
|
||||||
|
# if you opt to fill it out, you must fill it out _right_)
|
||||||
|
for index in range(self.initial_form_count()):
|
||||||
|
self.forms[index].use_required_attribute = True
|
||||||
|
|
||||||
|
def should_delete(self, cleaned):
|
||||||
|
"""Should this entry be deleted from the database?"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def pre_update(self, db_obj, cleaned):
|
||||||
|
"""Code to run before an item in the formset is saved."""
|
||||||
|
for key, value in cleaned.items():
|
||||||
|
setattr(db_obj, key, value)
|
||||||
|
|
||||||
|
def pre_create(self, db_obj, cleaned):
|
||||||
|
"""Code to run before an item in the formset is created in the database."""
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
def to_database(self, obj: DomainRequest):
|
||||||
|
"""
|
||||||
|
Adds this form's cleaned data to `obj` and saves `obj`.
|
||||||
|
|
||||||
|
Does nothing if form is not valid.
|
||||||
|
|
||||||
|
Hint: Subclass should call `self._to_database(...)`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _to_database(
|
||||||
|
self,
|
||||||
|
obj: DomainRequest,
|
||||||
|
join: str,
|
||||||
|
should_delete: Callable,
|
||||||
|
pre_update: Callable,
|
||||||
|
pre_create: Callable,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Performs the actual work of saving.
|
||||||
|
|
||||||
|
Has hooks such as `should_delete` and `pre_update` by which the
|
||||||
|
subclass can control behavior. Add more hooks whenever needed.
|
||||||
|
"""
|
||||||
|
if not self.is_valid():
|
||||||
|
return
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
query = getattr(obj, join).order_by("created_at").all() # order matters
|
||||||
|
|
||||||
|
# get the related name for the join defined for the db_obj for this form.
|
||||||
|
# the related name will be the reference on a related object back to db_obj
|
||||||
|
related_name = ""
|
||||||
|
field = obj._meta.get_field(join)
|
||||||
|
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
|
||||||
|
related_name = field.related_query_name()
|
||||||
|
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
|
||||||
|
related_name = field.related_query_name()
|
||||||
|
|
||||||
|
# the use of `zip` pairs the forms in the formset with the
|
||||||
|
# related objects gotten from the database -- there should always be
|
||||||
|
# at least as many forms as database entries: extra forms means new
|
||||||
|
# entries, but fewer forms is _not_ the correct way to delete items
|
||||||
|
# (likely a client-side error or an attempt at data tampering)
|
||||||
|
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
||||||
|
cleaned = post_data.cleaned_data if post_data is not None else {}
|
||||||
|
|
||||||
|
# matching database object exists, update it
|
||||||
|
if db_obj is not None and cleaned:
|
||||||
|
if should_delete(cleaned):
|
||||||
|
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
||||||
|
# Remove the specific relationship without deleting the object
|
||||||
|
getattr(db_obj, related_name).remove(self.domain_request)
|
||||||
|
else:
|
||||||
|
# If there are no other relationships, delete the object
|
||||||
|
db_obj.delete()
|
||||||
|
else:
|
||||||
|
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
||||||
|
# create a new db_obj and disconnect existing one
|
||||||
|
getattr(db_obj, related_name).remove(self.domain_request)
|
||||||
|
kwargs = pre_create(db_obj, cleaned)
|
||||||
|
getattr(obj, join).create(**kwargs)
|
||||||
|
else:
|
||||||
|
pre_update(db_obj, cleaned)
|
||||||
|
db_obj.save()
|
||||||
|
|
||||||
|
# no matching database object, create it
|
||||||
|
# make sure not to create a database object if cleaned has 'delete' attribute
|
||||||
|
elif db_obj is None and cleaned and not cleaned.get("DELETE", False):
|
||||||
|
kwargs = pre_create(db_obj, cleaned)
|
||||||
|
getattr(obj, join).create(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def on_fetch(cls, query):
|
||||||
|
"""Code to run when fetching formset's objects from the database."""
|
||||||
|
return query.values()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`."""
|
||||||
|
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDeletableRegistrarForm(RegistrarForm):
|
||||||
|
"""Adds special validation and delete functionality.
|
||||||
|
Used by forms that are tied to a Yes/No form."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.form_data_marked_for_deletion = False
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def mark_form_for_deletion(self):
|
||||||
|
"""Marks this form for deletion.
|
||||||
|
This changes behavior of validity checks and to_database
|
||||||
|
methods."""
|
||||||
|
self.form_data_marked_for_deletion = True
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""
|
||||||
|
This method overrides the default behavior for forms.
|
||||||
|
This cleans the form after field validation has already taken place.
|
||||||
|
In this override, remove errors associated with the form if form data
|
||||||
|
is marked for deletion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.form_data_marked_for_deletion:
|
||||||
|
# clear any errors raised by the form fields
|
||||||
|
# (before this clean() method is run, each field
|
||||||
|
# performs its own clean, which could result in
|
||||||
|
# errors that we wish to ignore at this point)
|
||||||
|
#
|
||||||
|
# NOTE: we cannot just clear() the errors list.
|
||||||
|
# That causes problems.
|
||||||
|
for field in self.fields:
|
||||||
|
if field in self.errors:
|
||||||
|
del self.errors[field]
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
def to_database(self, obj):
|
||||||
|
"""
|
||||||
|
This method overrides the behavior of RegistrarForm.
|
||||||
|
If form data is marked for deletion, set relevant fields
|
||||||
|
to None before saving.
|
||||||
|
Do nothing if form is not valid.
|
||||||
|
"""
|
||||||
|
if not self.is_valid():
|
||||||
|
return
|
||||||
|
if self.form_data_marked_for_deletion:
|
||||||
|
for field_name, _ in self.fields.items():
|
||||||
|
setattr(obj, field_name, None)
|
||||||
|
else:
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
setattr(obj, name, value)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseYesNoForm(RegistrarForm):
|
||||||
|
"""
|
||||||
|
Base class used for forms with a yes/no form with a hidden input on toggle.
|
||||||
|
Use this class when you need something similar to the AdditionalDetailsYesNoForm.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
form_is_checked (bool): Determines the default state (checked or not) of the Yes/No toggle.
|
||||||
|
field_name (str): Specifies the form field name that the Yes/No toggle controls.
|
||||||
|
required_error_message (str): Custom error message displayed when the field is required but not provided.
|
||||||
|
form_choices (tuple): Defines the choice options for the form field, defaulting to Yes/No choices.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
Subclass this form to implement specific Yes/No fields in various parts of the application, customizing
|
||||||
|
`form_is_checked` and `field_name` as necessary for the context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
form_is_checked: bool
|
||||||
|
|
||||||
|
# What field does the yes/no button hook to?
|
||||||
|
# For instance, this could be "has_other_contacts"
|
||||||
|
field_name: str
|
||||||
|
|
||||||
|
required_error_message = "This question is required."
|
||||||
|
|
||||||
|
# Default form choice mapping. Default is suitable for most cases.
|
||||||
|
form_choices = ((True, "Yes"), (False, "No"))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Extend the initialization of the form from RegistrarForm __init__"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields[self.field_name] = self.get_typed_choice_field()
|
||||||
|
|
||||||
|
def get_typed_choice_field(self):
|
||||||
|
"""
|
||||||
|
Creates a TypedChoiceField for the form with specified initial value and choices.
|
||||||
|
Returns:
|
||||||
|
TypedChoiceField: A Django form field specifically configured for selecting between
|
||||||
|
predefined choices with type coercion and custom error messages.
|
||||||
|
"""
|
||||||
|
choice_field = forms.TypedChoiceField(
|
||||||
|
coerce=lambda x: x.lower() == "true" if x is not None else None,
|
||||||
|
choices=self.form_choices,
|
||||||
|
initial=self.get_initial_value(),
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
error_messages={
|
||||||
|
"required": self.required_error_message,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return choice_field
|
||||||
|
|
||||||
|
def get_initial_value(self):
|
||||||
|
"""
|
||||||
|
Determines the initial value for TypedChoiceField.
|
||||||
|
More directly, this controls the "initial" field on forms.TypedChoiceField.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool | None: The initial value for the form field. If the domain request is set,
|
||||||
|
this will always return the value of self.form_is_checked.
|
||||||
|
Otherwise, None will be returned as a new domain request can't start out checked.
|
||||||
|
"""
|
||||||
|
# No pre-selection for new domain requests
|
||||||
|
initial_value = self.form_is_checked if self.domain_request else None
|
||||||
|
return initial_value
|
|
@ -0,0 +1,121 @@
|
||||||
|
""""
|
||||||
|
Data migration: Renaming deprecated Federal Agencies to
|
||||||
|
their new updated names ie (U.S. Peace Corps to Peace Corps)
|
||||||
|
within Domain Information and Domain Requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from registrar.models import DomainInformation, DomainRequest, FederalAgency
|
||||||
|
from registrar.management.commands.utility.terminal_helper import ScriptDataHelper
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Transfers Domain Request and Domain Information federal agency field from string to FederalAgency object"
|
||||||
|
|
||||||
|
# Deprecated federal agency names mapped to designated replacements {old_value, new value}
|
||||||
|
rename_deprecated_federal_agency = {
|
||||||
|
"Appraisal Subcommittee": "Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Program": "Barry Goldwater Scholarship and Excellence in Education Foundation", # noqa
|
||||||
|
"Federal Reserve System": "Federal Reserve Board of Governors",
|
||||||
|
"Harry S Truman Scholarship Foundation": "Harry S. Truman Scholarship Foundation",
|
||||||
|
"Japan-US Friendship Commission": "Japan-U.S. Friendship Commission",
|
||||||
|
"Japan-United States Friendship Commission": "Japan-U.S. Friendship Commission",
|
||||||
|
"John F. Kennedy Center for Performing Arts": "John F. Kennedy Center for the Performing Arts",
|
||||||
|
"Occupational Safety & Health Review Commission": "Occupational Safety and Health Review Commission",
|
||||||
|
"Corporation for National & Community Service": "Corporation for National and Community Service",
|
||||||
|
"Export/Import Bank of the U.S.": "Export-Import Bank of the United States",
|
||||||
|
"Medical Payment Advisory Commission": "Medicare Payment Advisory Commission",
|
||||||
|
"U.S. Peace Corps": "Peace Corps",
|
||||||
|
"Chemical Safety Board": "U.S. Chemical Safety Board",
|
||||||
|
"Nuclear Waste Technical Review Board": "U.S. Nuclear Waste Technical Review Board",
|
||||||
|
"State, Local, and Tribal Government": "Non-Federal Agency",
|
||||||
|
# "U.S. China Economic and Security Review Commission": "U.S.-China Economic and Security Review Commission",
|
||||||
|
}
|
||||||
|
|
||||||
|
def find_federal_agency_row(self, domain_object):
|
||||||
|
federal_agency = domain_object.federal_agency
|
||||||
|
# Domain Information objects without a federal agency default to Non-Federal Agency
|
||||||
|
if (federal_agency is None) or (federal_agency == ""):
|
||||||
|
federal_agency = "Non-Federal Agency"
|
||||||
|
if federal_agency in self.rename_deprecated_federal_agency.keys():
|
||||||
|
federal_agency = self.rename_deprecated_federal_agency[federal_agency]
|
||||||
|
return FederalAgency.objects.filter(agency=federal_agency).get()
|
||||||
|
|
||||||
|
def handle(self, **options):
|
||||||
|
"""
|
||||||
|
Renames the Federal Agency to the correct new naming
|
||||||
|
for both Domain Information and Domain Requests objects.
|
||||||
|
|
||||||
|
NOTE: If it's None for a domain request, we skip it as
|
||||||
|
a user most likely hasn't gotten to it yet.
|
||||||
|
"""
|
||||||
|
logger.info("Transferring federal agencies to FederalAgency object")
|
||||||
|
# DomainInformation object we populate with updated_federal_agency which are then bulk updated
|
||||||
|
domain_infos_to_update = []
|
||||||
|
domain_requests_to_update = []
|
||||||
|
# Domain Requests with null federal_agency that are not populated with updated_federal_agency
|
||||||
|
domain_requests_skipped = []
|
||||||
|
domain_infos_with_errors = []
|
||||||
|
domain_requests_with_errors = []
|
||||||
|
|
||||||
|
domain_infos = DomainInformation.objects.all()
|
||||||
|
domain_requests = DomainRequest.objects.all()
|
||||||
|
|
||||||
|
logger.info(f"Found {len(domain_infos)} DomainInfo objects with federal agency.")
|
||||||
|
logger.info(f"Found {len(domain_requests)} Domain Request objects with federal agency.")
|
||||||
|
|
||||||
|
for domain_info in domain_infos:
|
||||||
|
try:
|
||||||
|
federal_agency_row = self.find_federal_agency_row(domain_info)
|
||||||
|
domain_info.updated_federal_agency = federal_agency_row
|
||||||
|
domain_infos_to_update.append(domain_info)
|
||||||
|
logger.info(
|
||||||
|
f"DomainInformation {domain_info} => updated_federal_agency set to: \
|
||||||
|
{domain_info.updated_federal_agency}"
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
domain_infos_with_errors.append(domain_info)
|
||||||
|
logger.info(
|
||||||
|
f"DomainInformation {domain_info} failed to update updated_federal_agency \
|
||||||
|
from federal_agency {domain_info.federal_agency}. Error: {err}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ScriptDataHelper.bulk_update_fields(DomainInformation, domain_infos_to_update, ["updated_federal_agency"])
|
||||||
|
|
||||||
|
for domain_request in domain_requests:
|
||||||
|
try:
|
||||||
|
if (domain_request.federal_agency is None) or (domain_request.federal_agency == ""):
|
||||||
|
domain_requests_skipped.append(domain_request)
|
||||||
|
else:
|
||||||
|
federal_agency_row = self.find_federal_agency_row(domain_request)
|
||||||
|
domain_request.updated_federal_agency = federal_agency_row
|
||||||
|
domain_requests_to_update.append(domain_request)
|
||||||
|
logger.info(
|
||||||
|
f"DomainRequest {domain_request} => updated_federal_agency set to: \
|
||||||
|
{domain_request.updated_federal_agency}"
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
domain_requests_with_errors.append(domain_request)
|
||||||
|
logger.info(
|
||||||
|
f"DomainRequest {domain_request} failed to update updated_federal_agency \
|
||||||
|
from federal_agency {domain_request.federal_agency}. Error: {err}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ScriptDataHelper.bulk_update_fields(DomainRequest, domain_requests_to_update, ["updated_federal_agency"])
|
||||||
|
|
||||||
|
logger.info(f"{len(domain_infos_to_update)} DomainInformation rows updated update_federal_agency.")
|
||||||
|
logger.info(
|
||||||
|
f"{len(domain_infos_with_errors)} DomainInformation rows errored when updating update_federal_agency. \
|
||||||
|
{domain_infos_with_errors}"
|
||||||
|
)
|
||||||
|
logger.info(f"{len(domain_requests_to_update)} DomainRequest rows updated update_federal_agency.")
|
||||||
|
logger.info(f"{len(domain_requests_skipped)} DomainRequest rows with null federal_agency skipped.")
|
||||||
|
logger.info(
|
||||||
|
f"{len(domain_requests_with_errors)} DomainRequest rows errored when updating update_federal_agency. \
|
||||||
|
{domain_requests_with_errors}"
|
||||||
|
)
|
|
@ -0,0 +1,23 @@
|
||||||
|
import logging
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||||
|
from registrar.models import User
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand, PopulateScriptTemplate):
|
||||||
|
help = "Loops through each valid User object and updates its verification_type value"
|
||||||
|
|
||||||
|
def handle(self, **kwargs):
|
||||||
|
"""Loops through each valid User object and updates its verification_type value"""
|
||||||
|
filter_condition = {"verification_type__isnull": True}
|
||||||
|
self.mass_populate_field(User, filter_condition, ["verification_type"])
|
||||||
|
|
||||||
|
def populate_field(self, field_to_update):
|
||||||
|
"""Defines how we update the verification_type field"""
|
||||||
|
field_to_update.set_user_verification_type()
|
||||||
|
logger.info(
|
||||||
|
f"{TerminalColors.OKCYAN}Updating {field_to_update} => "
|
||||||
|
f"{field_to_update.verification_type}{TerminalColors.OKCYAN}"
|
||||||
|
)
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from typing import List
|
from typing import List
|
||||||
from registrar.utility.enums import LogCode
|
from registrar.utility.enums import LogCode
|
||||||
|
@ -58,6 +59,55 @@ class ScriptDataHelper:
|
||||||
model_class.objects.bulk_update(page.object_list, fields_to_update)
|
model_class.objects.bulk_update(page.object_list, fields_to_update)
|
||||||
|
|
||||||
|
|
||||||
|
class PopulateScriptTemplate(ABC):
|
||||||
|
"""
|
||||||
|
Contains an ABC for generic populate scripts
|
||||||
|
"""
|
||||||
|
|
||||||
|
def mass_populate_field(self, sender, filter_conditions, fields_to_update):
|
||||||
|
"""Loops through each valid "sender" object - specified by filter_conditions - and
|
||||||
|
updates fields defined by fields_to_update using populate_function.
|
||||||
|
|
||||||
|
You must define populate_field before you can use this function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
objects = sender.objects.filter(**filter_conditions)
|
||||||
|
|
||||||
|
# Code execution will stop here if the user prompts "N"
|
||||||
|
TerminalHelper.prompt_for_execution(
|
||||||
|
system_exit_on_terminate=True,
|
||||||
|
info_to_inspect=f"""
|
||||||
|
==Proposed Changes==
|
||||||
|
Number of {sender} objects to change: {len(objects)}
|
||||||
|
These fields will be updated on each record: {fields_to_update}
|
||||||
|
""",
|
||||||
|
prompt_title="Do you wish to patch this data?",
|
||||||
|
)
|
||||||
|
logger.info("Updating...")
|
||||||
|
|
||||||
|
to_update: List[sender] = []
|
||||||
|
failed_to_update: List[sender] = []
|
||||||
|
for updated_object in objects:
|
||||||
|
try:
|
||||||
|
self.populate_field(updated_object)
|
||||||
|
to_update.append(updated_object)
|
||||||
|
except Exception as err:
|
||||||
|
failed_to_update.append(updated_object)
|
||||||
|
logger.error(err)
|
||||||
|
logger.error(f"{TerminalColors.FAIL}" f"Failed to update {updated_object}" f"{TerminalColors.ENDC}")
|
||||||
|
|
||||||
|
# Do a bulk update on the first_ready field
|
||||||
|
ScriptDataHelper.bulk_update_fields(sender, to_update, fields_to_update)
|
||||||
|
|
||||||
|
# Log what happened
|
||||||
|
TerminalHelper.log_script_run_summary(to_update, failed_to_update, skipped=[], debug=True)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def populate_field(self, field_to_update):
|
||||||
|
"""Defines how we update each field. Must be defined before using mass_populate_field."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class TerminalHelper:
|
class TerminalHelper:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None):
|
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None):
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,47 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-04-25 16:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0087_alter_domain_deleted_alter_domain_expiration_date_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="cisa_representative_email",
|
||||||
|
field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="cisa_representative_email",
|
||||||
|
field=models.EmailField(blank=True, max_length=320, null=True, verbose_name="CISA regional representative"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="has_anything_else_text",
|
||||||
|
field=models.BooleanField(
|
||||||
|
blank=True, help_text="Determines if the user has a anything_else or not", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="has_cisa_representative",
|
||||||
|
field=models.BooleanField(
|
||||||
|
blank=True, help_text="Determines if the user has a representative email or not", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="anything_else",
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name="Additional details"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="anything_else",
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name="Additional details"),
|
||||||
|
),
|
||||||
|
]
|
29
src/registrar/migrations/0089_user_verification_type.py
Normal file
29
src/registrar/migrations/0089_user_verification_type.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-04-26 14:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0088_domaininformation_cisa_representative_email_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="verification_type",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("grandfathered", "Legacy user"),
|
||||||
|
("verified_by_staff", "Verified by staff"),
|
||||||
|
("regular", "Verified by Login.gov"),
|
||||||
|
("invited", "Invited by a domain manager"),
|
||||||
|
("fixture_user", "Created by fixtures"),
|
||||||
|
],
|
||||||
|
help_text="The means through which this user was verified",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -159,6 +159,31 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
return help_texts.get(state, "")
|
return help_texts.get(state, "")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_admin_help_text(cls, state):
|
||||||
|
"""Returns a help message for a desired state for /admin. If none is found, an empty string is returned"""
|
||||||
|
admin_help_texts = {
|
||||||
|
cls.UNKNOWN: (
|
||||||
|
"The creator of the associated domain request has not logged in to "
|
||||||
|
"manage the domain since it was approved. "
|
||||||
|
'The state will switch to "DNS needed" after they access the domain in the registrar.'
|
||||||
|
),
|
||||||
|
cls.DNS_NEEDED: (
|
||||||
|
"Before this domain can be used, name server addresses need to be added within the registrar."
|
||||||
|
),
|
||||||
|
cls.READY: "This domain has name servers and is ready for use.",
|
||||||
|
cls.ON_HOLD: (
|
||||||
|
"While on hold, this domain won't resolve in DNS and "
|
||||||
|
"any infrastructure (like websites) will be offline."
|
||||||
|
),
|
||||||
|
cls.DELETED: (
|
||||||
|
"This domain was permanently removed from the registry. "
|
||||||
|
"The domain no longer resolves in DNS and any infrastructure (like websites) is offline."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin_help_texts.get(state, "")
|
||||||
|
|
||||||
class Cache(property):
|
class Cache(property):
|
||||||
"""
|
"""
|
||||||
Python descriptor to turn class methods into properties.
|
Python descriptor to turn class methods into properties.
|
||||||
|
@ -992,22 +1017,25 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
blank=False,
|
blank=False,
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name="domain",
|
|
||||||
help_text="Fully qualified domain name",
|
help_text="Fully qualified domain name",
|
||||||
|
verbose_name="domain",
|
||||||
)
|
)
|
||||||
|
|
||||||
state = FSMField(
|
state = FSMField(
|
||||||
max_length=21,
|
max_length=21,
|
||||||
choices=State.choices,
|
choices=State.choices,
|
||||||
default=State.UNKNOWN,
|
default=State.UNKNOWN,
|
||||||
protected=True, # cannot change state directly, particularly in Django admin
|
# cannot change state directly, particularly in Django admin
|
||||||
|
protected=True,
|
||||||
|
# This must be defined for custom state help messages,
|
||||||
|
# as otherwise the view will purge the help field as it does not exist.
|
||||||
|
help_text=" ",
|
||||||
verbose_name="domain state",
|
verbose_name="domain state",
|
||||||
help_text="Very basic info about the lifecycle of this domain object",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expiration_date = DateField(
|
expiration_date = DateField(
|
||||||
null=True,
|
null=True,
|
||||||
help_text=("Duplication of registry's expiration date saved for ease of reporting"),
|
help_text=("Date the domain expires in the registry"),
|
||||||
)
|
)
|
||||||
|
|
||||||
security_contact_registry_id = TextField(
|
security_contact_registry_id = TextField(
|
||||||
|
@ -1019,15 +1047,15 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
deleted = DateField(
|
deleted = DateField(
|
||||||
null=True,
|
null=True,
|
||||||
editable=False,
|
editable=False,
|
||||||
|
help_text='Will appear blank unless the domain is in "deleted" state',
|
||||||
verbose_name="deleted on",
|
verbose_name="deleted on",
|
||||||
help_text="Deleted at date",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
first_ready = DateField(
|
first_ready = DateField(
|
||||||
null=True,
|
null=True,
|
||||||
editable=False,
|
editable=False,
|
||||||
|
help_text='Date when this domain first moved into "ready" state; date will never change',
|
||||||
verbose_name="first ready on",
|
verbose_name="first ready on",
|
||||||
help_text="The last time this domain moved into the READY state",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def isActive(self):
|
def isActive(self):
|
||||||
|
|
|
@ -47,6 +47,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name="information_created",
|
related_name="information_created",
|
||||||
|
help_text="Person who submitted the domain request",
|
||||||
)
|
)
|
||||||
|
|
||||||
domain_request = models.OneToOneField(
|
domain_request = models.OneToOneField(
|
||||||
|
@ -55,7 +56,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="DomainRequest_info",
|
related_name="DomainRequest_info",
|
||||||
help_text="Associated domain request",
|
help_text="Request associated with this domain",
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,7 +74,6 @@ class DomainInformation(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="election office",
|
verbose_name="election office",
|
||||||
help_text="Is your organization an election office?",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||||
|
@ -82,30 +82,26 @@ class DomainInformation(TimeStampedModel):
|
||||||
choices=DomainRequest.OrgChoicesElectionOffice.choices,
|
choices=DomainRequest.OrgChoicesElectionOffice.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Type of organization - Election office",
|
help_text='"Election" appears after the org type if it\'s an election office.',
|
||||||
)
|
)
|
||||||
|
|
||||||
federally_recognized_tribe = models.BooleanField(
|
federally_recognized_tribe = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Is the tribe federally recognized",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
state_recognized_tribe = models.BooleanField(
|
state_recognized_tribe = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Is the tribe recognized by a state",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tribe_name = models.CharField(
|
tribe_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Name of tribe",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
federal_agency = models.CharField(
|
federal_agency = models.CharField(
|
||||||
choices=AGENCY_CHOICES,
|
choices=AGENCY_CHOICES,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Federal agency",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
federal_type = models.CharField(
|
federal_type = models.CharField(
|
||||||
|
@ -113,38 +109,32 @@ class DomainInformation(TimeStampedModel):
|
||||||
choices=BranchChoices.choices,
|
choices=BranchChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Federal government branch",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is_election_board = models.BooleanField(
|
is_election_board = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="election office",
|
verbose_name="election office",
|
||||||
help_text="Is your organization an election office?",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
organization_name = models.CharField(
|
organization_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Organization name",
|
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
address_line1 = models.CharField(
|
address_line1 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
|
||||||
verbose_name="address line 1",
|
verbose_name="address line 1",
|
||||||
)
|
)
|
||||||
address_line2 = models.CharField(
|
address_line2 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address line 2 (optional)",
|
|
||||||
verbose_name="address line 2",
|
verbose_name="address line 2",
|
||||||
)
|
)
|
||||||
city = models.CharField(
|
city = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="City",
|
|
||||||
)
|
)
|
||||||
state_territory = models.CharField(
|
state_territory = models.CharField(
|
||||||
max_length=2,
|
max_length=2,
|
||||||
|
@ -152,27 +142,24 @@ class DomainInformation(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="state / territory",
|
verbose_name="state / territory",
|
||||||
help_text="State, territory, or military post",
|
|
||||||
)
|
)
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Zip code",
|
|
||||||
verbose_name="zip code",
|
|
||||||
db_index=True,
|
db_index=True,
|
||||||
|
verbose_name="zip code",
|
||||||
)
|
)
|
||||||
urbanization = models.CharField(
|
urbanization = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Urbanization (required for Puerto Rico only)",
|
help_text="Required for Puerto Rico only",
|
||||||
verbose_name="urbanization",
|
verbose_name="urbanization",
|
||||||
)
|
)
|
||||||
|
|
||||||
about_your_organization = models.TextField(
|
about_your_organization = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Information about your organization",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
authorizing_official = models.ForeignKey(
|
authorizing_official = models.ForeignKey(
|
||||||
|
@ -190,7 +177,6 @@ class DomainInformation(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
# Access this information via Domain as "domain.domain_info"
|
# Access this information via Domain as "domain.domain_info"
|
||||||
related_name="domain_info",
|
related_name="domain_info",
|
||||||
help_text="Domain to which this information belongs",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# This is the contact information provided by the domain requestor. The
|
# This is the contact information provided by the domain requestor. The
|
||||||
|
@ -201,6 +187,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="submitted_domain_requests_information",
|
related_name="submitted_domain_requests_information",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
help_text='Person listed under "your contact information" in the request form',
|
||||||
)
|
)
|
||||||
|
|
||||||
purpose = models.TextField(
|
purpose = models.TextField(
|
||||||
|
@ -219,13 +206,20 @@ class DomainInformation(TimeStampedModel):
|
||||||
no_other_contacts_rationale = models.TextField(
|
no_other_contacts_rationale = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Reason for listing no additional contacts",
|
help_text="Required if creator does not list other employees",
|
||||||
)
|
)
|
||||||
|
|
||||||
anything_else = models.TextField(
|
anything_else = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Anything else?",
|
verbose_name="Additional details",
|
||||||
|
)
|
||||||
|
|
||||||
|
cisa_representative_email = models.EmailField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="CISA regional representative",
|
||||||
|
max_length=320,
|
||||||
)
|
)
|
||||||
|
|
||||||
is_policy_acknowledged = models.BooleanField(
|
is_policy_acknowledged = models.BooleanField(
|
||||||
|
@ -237,7 +231,6 @@ class DomainInformation(TimeStampedModel):
|
||||||
notes = models.TextField(
|
notes = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Notes about the request",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -16,12 +16,21 @@ from .utility.time_stamped_model import TimeStampedModel
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
from auditlog.models import AuditlogHistoryField # type: ignore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DomainRequest(TimeStampedModel):
|
class DomainRequest(TimeStampedModel):
|
||||||
"""A registrant's domain request for a new domain."""
|
"""A registrant's domain request for a new domain."""
|
||||||
|
|
||||||
|
# https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history
|
||||||
|
# If we note any performace degradation due to this addition,
|
||||||
|
# we can query the auditlogs table in admin.py and add the results to
|
||||||
|
# extra_context in the change_view method for DomainRequestAdmin.
|
||||||
|
# This is the more straightforward way so trying it first.
|
||||||
|
history = AuditlogHistoryField()
|
||||||
|
|
||||||
# Constants for choice fields
|
# Constants for choice fields
|
||||||
class DomainRequestStatus(models.TextChoices):
|
class DomainRequestStatus(models.TextChoices):
|
||||||
STARTED = "started", "Started"
|
STARTED = "started", "Started"
|
||||||
|
@ -464,6 +473,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name="domain_requests_created",
|
related_name="domain_requests_created",
|
||||||
|
help_text="Person who submitted the domain request; will not receive email updates",
|
||||||
)
|
)
|
||||||
|
|
||||||
investigator = models.ForeignKey(
|
investigator = models.ForeignKey(
|
||||||
|
@ -481,14 +491,12 @@ class DomainRequest(TimeStampedModel):
|
||||||
choices=OrganizationChoices.choices,
|
choices=OrganizationChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Type of organization",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is_election_board = models.BooleanField(
|
is_election_board = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="election office",
|
verbose_name="election office",
|
||||||
help_text="Is your organization an election office?",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||||
|
@ -497,30 +505,26 @@ class DomainRequest(TimeStampedModel):
|
||||||
choices=OrgChoicesElectionOffice.choices,
|
choices=OrgChoicesElectionOffice.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Type of organization - Election office",
|
help_text='"Election" appears after the org type if it\'s an election office.',
|
||||||
)
|
)
|
||||||
|
|
||||||
federally_recognized_tribe = models.BooleanField(
|
federally_recognized_tribe = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Is the tribe federally recognized",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
state_recognized_tribe = models.BooleanField(
|
state_recognized_tribe = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Is the tribe recognized by a state",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tribe_name = models.CharField(
|
tribe_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Name of tribe",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
federal_agency = models.CharField(
|
federal_agency = models.CharField(
|
||||||
choices=AGENCY_CHOICES,
|
choices=AGENCY_CHOICES,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Federal agency",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
federal_type = models.CharField(
|
federal_type = models.CharField(
|
||||||
|
@ -528,32 +532,27 @@ class DomainRequest(TimeStampedModel):
|
||||||
choices=BranchChoices.choices,
|
choices=BranchChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Federal government branch",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
organization_name = models.CharField(
|
organization_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Organization name",
|
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
address_line1 = models.CharField(
|
address_line1 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
|
||||||
verbose_name="Address line 1",
|
verbose_name="Address line 1",
|
||||||
)
|
)
|
||||||
address_line2 = models.CharField(
|
address_line2 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address line 2 (optional)",
|
|
||||||
verbose_name="Address line 2",
|
verbose_name="Address line 2",
|
||||||
)
|
)
|
||||||
city = models.CharField(
|
city = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="City",
|
|
||||||
)
|
)
|
||||||
state_territory = models.CharField(
|
state_territory = models.CharField(
|
||||||
max_length=2,
|
max_length=2,
|
||||||
|
@ -561,26 +560,23 @@ class DomainRequest(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="state / territory",
|
verbose_name="state / territory",
|
||||||
help_text="State, territory, or military post",
|
|
||||||
)
|
)
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="zip code",
|
verbose_name="zip code",
|
||||||
help_text="Zip code",
|
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
urbanization = models.CharField(
|
urbanization = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Urbanization (required for Puerto Rico only)",
|
help_text="Required for Puerto Rico only",
|
||||||
)
|
)
|
||||||
|
|
||||||
about_your_organization = models.TextField(
|
about_your_organization = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Information about your organization",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
authorizing_official = models.ForeignKey(
|
authorizing_official = models.ForeignKey(
|
||||||
|
@ -603,7 +599,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
"Domain",
|
"Domain",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="The approved domain",
|
help_text="Domain associated with this request; will be blank until request is approved",
|
||||||
related_name="domain_request",
|
related_name="domain_request",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
@ -612,7 +608,6 @@ class DomainRequest(TimeStampedModel):
|
||||||
"DraftDomain",
|
"DraftDomain",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="The requested domain",
|
|
||||||
related_name="domain_request",
|
related_name="domain_request",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
@ -621,6 +616,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
"registrar.Website",
|
"registrar.Website",
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="alternatives+",
|
related_name="alternatives+",
|
||||||
|
help_text="Other domain names the creator provided for consideration",
|
||||||
)
|
)
|
||||||
|
|
||||||
# This is the contact information provided by the domain requestor. The
|
# This is the contact information provided by the domain requestor. The
|
||||||
|
@ -631,12 +627,12 @@ class DomainRequest(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="submitted_domain_requests",
|
related_name="submitted_domain_requests",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
help_text='Person listed under "your contact information" in the request form; will receive email updates',
|
||||||
)
|
)
|
||||||
|
|
||||||
purpose = models.TextField(
|
purpose = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Purpose of your domain",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
other_contacts = models.ManyToManyField(
|
other_contacts = models.ManyToManyField(
|
||||||
|
@ -649,13 +645,38 @@ class DomainRequest(TimeStampedModel):
|
||||||
no_other_contacts_rationale = models.TextField(
|
no_other_contacts_rationale = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Reason for listing no additional contacts",
|
help_text="Required if creator does not list other employees",
|
||||||
)
|
)
|
||||||
|
|
||||||
anything_else = models.TextField(
|
anything_else = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Anything else?",
|
verbose_name="Additional details",
|
||||||
|
)
|
||||||
|
|
||||||
|
# This is a drop-in replacement for a has_anything_else_text() function.
|
||||||
|
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
||||||
|
# a tertiary state. We should not display this in /admin.
|
||||||
|
has_anything_else_text = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Determines if the user has a anything_else or not",
|
||||||
|
)
|
||||||
|
|
||||||
|
cisa_representative_email = models.EmailField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="CISA regional representative",
|
||||||
|
max_length=320,
|
||||||
|
)
|
||||||
|
|
||||||
|
# This is a drop-in replacement for an has_cisa_representative() function.
|
||||||
|
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
||||||
|
# a tertiary state. We should not display this in /admin.
|
||||||
|
has_cisa_representative = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Determines if the user has a representative email or not",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_policy_acknowledged = models.BooleanField(
|
is_policy_acknowledged = models.BooleanField(
|
||||||
|
@ -676,7 +697,6 @@ class DomainRequest(TimeStampedModel):
|
||||||
notes = models.TextField(
|
notes = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Notes about this request",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_organization_type(self):
|
def sync_organization_type(self):
|
||||||
|
@ -711,8 +731,33 @@ class DomainRequest(TimeStampedModel):
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Save override for custom properties"""
|
"""Save override for custom properties"""
|
||||||
self.sync_organization_type()
|
self.sync_organization_type()
|
||||||
|
self.sync_yes_no_form_fields()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def sync_yes_no_form_fields(self):
|
||||||
|
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||||
|
We handle that here for def save().
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This ensures that if we have prefilled data, the form is prepopulated
|
||||||
|
if self.cisa_representative_email is not None:
|
||||||
|
self.has_cisa_representative = self.cisa_representative_email != ""
|
||||||
|
|
||||||
|
# This check is required to ensure that the form doesn't start out checked
|
||||||
|
if self.has_cisa_representative is not None:
|
||||||
|
self.has_cisa_representative = (
|
||||||
|
self.cisa_representative_email != "" and self.cisa_representative_email is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
# This ensures that if we have prefilled data, the form is prepopulated
|
||||||
|
if self.anything_else is not None:
|
||||||
|
self.has_anything_else_text = self.anything_else != ""
|
||||||
|
|
||||||
|
# This check is required to ensure that the form doesn't start out checked.
|
||||||
|
if self.has_anything_else_text is not None:
|
||||||
|
self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
try:
|
try:
|
||||||
if self.requested_domain and self.requested_domain.name:
|
if self.requested_domain and self.requested_domain.name:
|
||||||
|
@ -1051,6 +1096,16 @@ class DomainRequest(TimeStampedModel):
|
||||||
"""Does this domain request have other contacts listed?"""
|
"""Does this domain request have other contacts listed?"""
|
||||||
return self.other_contacts.exists()
|
return self.other_contacts.exists()
|
||||||
|
|
||||||
|
def has_additional_details(self) -> bool:
|
||||||
|
"""Combines the has_anything_else_text and has_cisa_representative fields,
|
||||||
|
then returns if this domain request has either of them."""
|
||||||
|
# Split out for linter
|
||||||
|
has_details = False
|
||||||
|
if self.has_anything_else_text or self.has_cisa_representative:
|
||||||
|
has_details = True
|
||||||
|
|
||||||
|
return has_details
|
||||||
|
|
||||||
def is_federal(self) -> Union[bool, None]:
|
def is_federal(self) -> Union[bool, None]:
|
||||||
"""Is this domain request for a federal agency?
|
"""Is this domain request for a federal agency?
|
||||||
|
|
||||||
|
|
|
@ -22,14 +22,13 @@ class Host(TimeStampedModel):
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
unique=False,
|
unique=False,
|
||||||
verbose_name="host name",
|
verbose_name="host name",
|
||||||
help_text="Fully qualified domain name",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
domain = models.ForeignKey(
|
domain = models.ForeignKey(
|
||||||
"registrar.Domain",
|
"registrar.Domain",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name="host", # access this Host via the Domain as `domain.host`
|
related_name="host", # access this Host via the Domain as `domain.host`
|
||||||
help_text="Domain to which this host belongs",
|
help_text="Domain associated with this host",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -21,12 +21,11 @@ class HostIP(TimeStampedModel):
|
||||||
default=None, # prevent saving without a value
|
default=None, # prevent saving without a value
|
||||||
validators=[validate_ipv46_address],
|
validators=[validate_ipv46_address],
|
||||||
verbose_name="IP address",
|
verbose_name="IP address",
|
||||||
help_text="IP address",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
host = models.ForeignKey(
|
host = models.ForeignKey(
|
||||||
"registrar.Host",
|
"registrar.Host",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name="ip", # access this HostIP via the Host as `host.ip`
|
related_name="ip", # access this HostIP via the Host as `host.ip`
|
||||||
help_text="Host to which this IP address belongs",
|
help_text="IP associated with this host",
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
|
||||||
|
@ -23,6 +24,28 @@ class User(AbstractUser):
|
||||||
but can be customized later.
|
but can be customized later.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class VerificationTypeChoices(models.TextChoices):
|
||||||
|
"""
|
||||||
|
Users achieve access to our system in a few different ways.
|
||||||
|
These choices reflect those pathways.
|
||||||
|
|
||||||
|
Overview of verification types:
|
||||||
|
- GRANDFATHERED: User exists in the `TransitionDomain` table
|
||||||
|
- VERIFIED_BY_STAFF: User exists in the `VerifiedByStaff` table
|
||||||
|
- INVITED: User exists in the `DomainInvitation` table
|
||||||
|
- REGULAR: User was verified through IAL2
|
||||||
|
- FIXTURE_USER: User was created by fixtures
|
||||||
|
"""
|
||||||
|
|
||||||
|
GRANDFATHERED = "grandfathered", "Legacy user"
|
||||||
|
VERIFIED_BY_STAFF = "verified_by_staff", "Verified by staff"
|
||||||
|
REGULAR = "regular", "Verified by Login.gov"
|
||||||
|
INVITED = "invited", "Invited by a domain manager"
|
||||||
|
# We need a type for fixture users (rather than using verified by staff)
|
||||||
|
# because those users still do get "verified" through normal means
|
||||||
|
# after they login.
|
||||||
|
FIXTURE_USER = "fixture_user", "Created by fixtures"
|
||||||
|
|
||||||
# #### Constants for choice fields ####
|
# #### Constants for choice fields ####
|
||||||
RESTRICTED = "restricted"
|
RESTRICTED = "restricted"
|
||||||
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
|
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
|
||||||
|
@ -34,6 +57,7 @@ class User(AbstractUser):
|
||||||
null=True, # Allow the field to be null
|
null=True, # Allow the field to be null
|
||||||
blank=True, # Allow the field to be blank
|
blank=True, # Allow the field to be blank
|
||||||
verbose_name="user status",
|
verbose_name="user status",
|
||||||
|
help_text='Users in "restricted" status cannot make updates in the registrar or start a new request.',
|
||||||
)
|
)
|
||||||
|
|
||||||
domains = models.ManyToManyField(
|
domains = models.ManyToManyField(
|
||||||
|
@ -49,6 +73,13 @@ class User(AbstractUser):
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
verification_type = models.CharField(
|
||||||
|
choices=VerificationTypeChoices.choices,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="The means through which this user was verified",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# this info is pulled from Login.gov
|
# this info is pulled from Login.gov
|
||||||
if self.first_name or self.last_name:
|
if self.first_name or self.last_name:
|
||||||
|
@ -113,23 +144,61 @@ class User(AbstractUser):
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
# We can't set the verification type here because the user may not
|
||||||
|
# always exist at this point. We do it down the line.
|
||||||
|
verification_type = cls.get_verification_type_from_email(email)
|
||||||
|
|
||||||
|
# Checks if the user needs verification.
|
||||||
|
# The user needs identity verification if they don't meet
|
||||||
|
# any special criteria, i.e. we are validating them "regularly"
|
||||||
|
return verification_type == cls.VerificationTypeChoices.REGULAR
|
||||||
|
|
||||||
|
def set_user_verification_type(self):
|
||||||
|
"""
|
||||||
|
Given pre-existing data from TransitionDomain, VerifiedByStaff, and DomainInvitation,
|
||||||
|
set the verification "type" defined in VerificationTypeChoices.
|
||||||
|
"""
|
||||||
|
email_or_username = self.email if self.email else self.username
|
||||||
|
retrieved = DomainInvitation.DomainInvitationStatus.RETRIEVED
|
||||||
|
verification_type = self.get_verification_type_from_email(email_or_username, invitation_status=retrieved)
|
||||||
|
|
||||||
|
# An existing user may have been invited to a domain after they got verified.
|
||||||
|
# We need to check for this condition.
|
||||||
|
if verification_type == User.VerificationTypeChoices.INVITED:
|
||||||
|
invitation = (
|
||||||
|
DomainInvitation.objects.filter(email=email_or_username, status=retrieved)
|
||||||
|
.order_by("created_at")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
# If you joined BEFORE the oldest invitation was created, then you were verified normally.
|
||||||
|
# (See logic in get_verification_type_from_email)
|
||||||
|
if not invitation and self.date_joined < invitation.created_at:
|
||||||
|
verification_type = User.VerificationTypeChoices.REGULAR
|
||||||
|
|
||||||
|
self.verification_type = verification_type
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_verification_type_from_email(cls, email, invitation_status=DomainInvitation.DomainInvitationStatus.INVITED):
|
||||||
|
"""Retrieves the verification type based off of a provided email address"""
|
||||||
|
|
||||||
|
verification_type = None
|
||||||
|
if TransitionDomain.objects.filter(Q(username=email) | Q(email=email)).exists():
|
||||||
# A new incoming user who is a domain manager for one of the domains
|
# A new incoming user who is a domain manager for one of the domains
|
||||||
# that we inputted from Verisign (that is, their email address appears
|
# that we inputted from Verisign (that is, their email address appears
|
||||||
# in the username field of a TransitionDomain)
|
# in the username field of a TransitionDomain)
|
||||||
if TransitionDomain.objects.filter(username=email).exists():
|
verification_type = cls.VerificationTypeChoices.GRANDFATHERED
|
||||||
return False
|
elif VerifiedByStaff.objects.filter(email=email).exists():
|
||||||
|
|
||||||
# New users flagged by Staff to bypass ial2
|
# New users flagged by Staff to bypass ial2
|
||||||
if VerifiedByStaff.objects.filter(email=email).exists():
|
verification_type = cls.VerificationTypeChoices.VERIFIED_BY_STAFF
|
||||||
return False
|
elif DomainInvitation.objects.filter(email=email, status=invitation_status).exists():
|
||||||
|
|
||||||
# A new incoming user who is being invited to be a domain manager (that is,
|
# A new incoming user who is being invited to be a domain manager (that is,
|
||||||
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
|
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
|
||||||
invited = DomainInvitation.DomainInvitationStatus.INVITED
|
verification_type = cls.VerificationTypeChoices.INVITED
|
||||||
if DomainInvitation.objects.filter(email=email, status=invited).exists():
|
else:
|
||||||
return False
|
verification_type = cls.VerificationTypeChoices.REGULAR
|
||||||
|
|
||||||
return True
|
return verification_type
|
||||||
|
|
||||||
def check_domain_invitations_on_login(self):
|
def check_domain_invitations_on_login(self):
|
||||||
"""When a user first arrives on the site, we need to retrieve any domain
|
"""When a user first arrives on the site, we need to retrieve any domain
|
||||||
|
|
|
@ -167,7 +167,7 @@ class CreateOrUpdateOrganizationTypeHelper:
|
||||||
# There is no avenue for this to occur in the UI,
|
# There is no avenue for this to occur in the UI,
|
||||||
# as such - this can only occur if the object is initialized in this way.
|
# as such - this can only occur if the object is initialized in this way.
|
||||||
# Or if there are pre-existing data.
|
# Or if there are pre-existing data.
|
||||||
logger.warning(
|
logger.debug(
|
||||||
"create_or_update_organization_type() -> is_election_board "
|
"create_or_update_organization_type() -> is_election_board "
|
||||||
f"cannot exist for {generic_org_type}. Setting to None."
|
f"cannot exist for {generic_org_type}. Setting to None."
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,6 @@ class VerifiedByStaff(TimeStampedModel):
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
help_text="Email",
|
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,12 +18,12 @@ class VerifiedByStaff(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="verifiedby_user",
|
related_name="verifiedby_user",
|
||||||
|
help_text="Person who verified this user",
|
||||||
)
|
)
|
||||||
|
|
||||||
notes = models.TextField(
|
notes = models.TextField(
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
help_text="Notes",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Website(TimeStampedModel):
|
||||||
website = models.CharField(
|
website = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
null=False,
|
null=False,
|
||||||
help_text="",
|
help_text="An alternative domain or current website listed on a domain request",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||||
<script type="application/javascript" src="{% static 'js/get-gov-reports.js' %}" defer></script>
|
<script type="application/javascript" src="{% static 'js/get-gov-reports.js' %}" defer></script>
|
||||||
|
<script type="application/javascript" src="{% static 'js/dja-collapse.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||||
|
|
|
@ -6,9 +6,23 @@ It is not inherently customizable on its own, so we can modify this instead.
|
||||||
https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/includes/fieldset.html
|
https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/includes/fieldset.html
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<fieldset class="module aligned {{ fieldset.classes }}">
|
<fieldset class="module aligned {{ fieldset.classes }}">
|
||||||
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
|
{% if fieldset.name %}
|
||||||
|
{# Customize the markup for the collapse toggle #}
|
||||||
|
{% if 'collapse--dotgov' in fieldset.classes %}
|
||||||
|
<button type="button">
|
||||||
|
<span>{{ fieldset.name }}</span>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<legend class="sr-only">{{ fieldset.description }}</legend>
|
||||||
|
{% else %}
|
||||||
|
<h2>{{ fieldset.name }}</h2>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if fieldset.description %}
|
{# Customize the markup for the collapse toggle: Do not show a description for the collapse fieldsets, instead we're using the description as a screen reader only legend #}
|
||||||
|
{% if fieldset.description and 'collapse--dotgov' not in fieldset.classes %}
|
||||||
<div class="description">{{ fieldset.description|safe }}</div>
|
<div class="description">{{ fieldset.description|safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ block.super }}
|
|
||||||
|
{% for fieldset in adminform %}
|
||||||
|
{% include "django/admin/includes/domain_fieldset.html" with state_help_message=state_help_message %}
|
||||||
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block submit_buttons_bottom %}
|
{% block submit_buttons_bottom %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user.has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list">
|
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user.has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list">
|
||||||
|
|
||||||
{% if show_formatted_name %}
|
{% if show_formatted_name %}
|
||||||
{% if contact.get_formatted_name %}
|
{% if user.get_formatted_name %}
|
||||||
<a href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a><br />
|
<a href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a><br />
|
||||||
{% else %}
|
{% else %}
|
||||||
None<br />
|
None<br />
|
||||||
|
@ -47,7 +47,12 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
None<br>
|
None<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
No additional contact information found.
|
No additional contact information found.<br>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_verification_type %}
|
||||||
|
{{ user_verification_type }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</address>
|
</address>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
{% comment %}
|
{% comment %}
|
||||||
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% block field_readonly %}
|
{% block field_readonly %}
|
||||||
{% with all_contacts=original_object.other_contacts.all %}
|
{% with all_contacts=original_object.other_contacts.all %}
|
||||||
{% if field.field.name == "other_contacts" %}
|
{% if field.field.name == "other_contacts" %}
|
||||||
|
@ -66,14 +67,42 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endblock field_readonly %}
|
{% endblock field_readonly %}
|
||||||
|
|
||||||
{% block after_help_text %}
|
{% block after_help_text %}
|
||||||
{% if field.field.name == "creator" %}
|
{% if field.field.name == "status" and original_object.history.count > 0 %}
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
|
<label aria-label="Submitter contact details"></label>
|
||||||
|
<div class="usa-table-container--scrollable" tabindex="0">
|
||||||
|
<table class="usa-table usa-table--borderless">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Changed at</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log_entry in original_object.history.all %}
|
||||||
|
{% for key, value in log_entry.changes_display_dict.items %}
|
||||||
|
{% if key == "status" %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ value.1|default:"None" }}</td>
|
||||||
|
<td>{{ log_entry.actor|default:"None" }}</td>
|
||||||
|
<td>{{ log_entry.timestamp|default:"None" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif field.field.name == "creator" %}
|
||||||
|
<div class="flex-container tablet:margin-top-2">
|
||||||
<label aria-label="Creator contact details"></label>
|
<label aria-label="Creator contact details"></label>
|
||||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
|
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%}
|
||||||
</div>
|
</div>
|
||||||
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
|
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
|
||||||
{% elif field.field.name == "submitter" %}
|
{% elif field.field.name == "submitter" %}
|
||||||
<div class="flex-container">
|
<div class="flex-container tablet:margin-top-2">
|
||||||
<label aria-label="Submitter contact details"></label>
|
<label aria-label="Submitter contact details"></label>
|
||||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
|
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "admin/fieldset.html" %}
|
||||||
|
{% load static url_helpers %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block field_readonly %}
|
||||||
|
{% if field.field.name == "state" %}
|
||||||
|
<div class="readonly">{{ domain_state }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="readonly">{{ field.contents }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block help_text %}
|
||||||
|
<div class="help margin-bottom-1" {% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
|
||||||
|
{% if field.field.name == "state" %}
|
||||||
|
<div>{{ state_help_message }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div>{{ field.field.help_text|safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock help_text %}
|
|
@ -0,0 +1,55 @@
|
||||||
|
{% extends 'domain_request_form.html' %}
|
||||||
|
{% load static field_helpers %}
|
||||||
|
|
||||||
|
{% block form_instructions %}
|
||||||
|
<em>These questions are required (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form_required_fields_help_text %}
|
||||||
|
{# commented out so it does not appear at this point on this page #}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- TODO-NL: (refactor) Breakup into two separate components-->
|
||||||
|
{% block form_fields %}
|
||||||
|
<fieldset class="usa-fieldset margin-top-2">
|
||||||
|
<legend>
|
||||||
|
<h2>Are you working with a CISA regional representative on your domain request?</h2>
|
||||||
|
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has <a href="https://www.cisa.gov/about/regions" target="_blank">10 regions</a> that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<!-- Toggle -->
|
||||||
|
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors forms.0.has_cisa_representative %}
|
||||||
|
{% endwith %}
|
||||||
|
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||||
|
<!-- TODO-NL: Hookup forms.0 to yes/no form for cisa representative (backend def)-->
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div id="cisa-representative" class="cisa-representative-form">
|
||||||
|
{% input_with_errors forms.1.cisa_representative_email %}
|
||||||
|
{# forms.1 is a form for inputting the e-mail of a cisa representative #}
|
||||||
|
<!-- TODO-NL: Hookup forms.1 to cisa representative form (backend def) -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<fieldset class="usa-fieldset margin-top-2">
|
||||||
|
<legend>
|
||||||
|
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<!-- Toggle -->
|
||||||
|
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors forms.2.has_anything_else_text %}
|
||||||
|
{% endwith %}
|
||||||
|
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||||
|
<!-- TODO-NL: Hookup forms.2 to yes/no form for anything else form (backend def)-->
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div id="anything-else">
|
||||||
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors forms.3.anything_else %}
|
||||||
|
{% endwith %}
|
||||||
|
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
|
||||||
|
<!-- TODO-NL: Hookup forms.3 to anything else form (backend def) -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,19 +0,0 @@
|
||||||
{% extends 'domain_request_form.html' %}
|
|
||||||
{% load field_helpers %}
|
|
||||||
|
|
||||||
{% block form_instructions %}
|
|
||||||
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
|
||||||
|
|
||||||
<p>This question is optional.</p>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block form_required_fields_help_text %}
|
|
||||||
{# commented out so it does not appear on this page #}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block form_fields %}
|
|
||||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
|
||||||
{% input_with_errors forms.0.anything_else %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endblock %}
|
|
|
@ -155,11 +155,20 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if step == Step.ANYTHING_ELSE %}
|
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"No" %}
|
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
<h3 class="register-form-review-header">Anything else</h3>
|
||||||
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
|
{% if domain_request.anything_else %}
|
||||||
|
{{domain_request.anything_else}}
|
||||||
|
{% else %}
|
||||||
|
No
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,18 @@
|
||||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include "includes/summary_item.html" with title='Anything else?' value=DomainRequest.anything_else|default:"No" heading_level=heading_level %}
|
{# We always show this field even if None #}
|
||||||
|
{% if DomainRequest %}
|
||||||
|
{% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regional representative' value=DomainRequest.cisa_representative_email custom_text_for_value_none='No' heading_level=heading_level %}
|
||||||
|
<h3 class="register-form-review-header">Anything else</h3>
|
||||||
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
|
{% if DomainRequest.anything_else %}
|
||||||
|
{{DomainRequest.anything_else}}
|
||||||
|
{% else %}
|
||||||
|
No
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<section class="section--outlined">
|
<section class="section--outlined">
|
||||||
<h2>Domains</h2>
|
<h2>Domains</h2>
|
||||||
{% if domains %}
|
{% if domains %}
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__registered-domains">
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -98,13 +98,21 @@
|
||||||
></div>
|
></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>You don't have any registered domains.</p>
|
<p>You don't have any registered domains.</p>
|
||||||
|
<p class="maxw-none clearfix">
|
||||||
|
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#help_outline"></use>
|
||||||
|
</svg>
|
||||||
|
Why don't I see my domain when I sign in to the registrar?
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section--outlined">
|
<section class="section--outlined">
|
||||||
<h2>Domain requests</h2>
|
<h2>Domain requests</h2>
|
||||||
{% if domain_requests %}
|
{% if domain_requests %}
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__domain-requests">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
</{{ heading_level }}>
|
</{{ heading_level }}>
|
||||||
{% else %}
|
{% else %}
|
||||||
</h2>
|
</h2>
|
||||||
|
{% endif %}
|
||||||
|
{% if sub_header_text %}
|
||||||
|
<h3 class="register-form-review-header">{{ sub_header_text }}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if address %}
|
{% if address %}
|
||||||
{% include "includes/organization_address.html" with organization=value %}
|
{% include "includes/organization_address.html" with organization=value %}
|
||||||
|
@ -39,6 +42,10 @@
|
||||||
</dd>
|
</dd>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</dl>
|
||||||
|
{% elif custom_text_for_value_none %}
|
||||||
|
<p>
|
||||||
|
{{ custom_text_for_value_none }}
|
||||||
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
None
|
None
|
||||||
|
@ -92,6 +99,8 @@
|
||||||
<p class="margin-top-0 margin-bottom-0">
|
<p class="margin-top-0 margin-bottom-0">
|
||||||
{% if value %}
|
{% if value %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
|
{% elif custom_text_for_value_none %}
|
||||||
|
{{ custom_text_for_value_none }}
|
||||||
{% else %}
|
{% else %}
|
||||||
None
|
None
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -789,6 +789,7 @@ def create_ready_domain():
|
||||||
return domain
|
return domain
|
||||||
|
|
||||||
|
|
||||||
|
# TODO in 1793: Remove the federal agency/updated federal agency fields
|
||||||
def completed_domain_request(
|
def completed_domain_request(
|
||||||
has_other_contacts=True,
|
has_other_contacts=True,
|
||||||
has_current_website=True,
|
has_current_website=True,
|
||||||
|
@ -803,6 +804,8 @@ def completed_domain_request(
|
||||||
generic_org_type="federal",
|
generic_org_type="federal",
|
||||||
is_election_board=False,
|
is_election_board=False,
|
||||||
organization_type=None,
|
organization_type=None,
|
||||||
|
federal_agency=None,
|
||||||
|
updated_federal_agency=None,
|
||||||
):
|
):
|
||||||
"""A completed domain request."""
|
"""A completed domain request."""
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -839,6 +842,7 @@ def completed_domain_request(
|
||||||
last_name="Bob",
|
last_name="Bob",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
domain_request_kwargs = dict(
|
domain_request_kwargs = dict(
|
||||||
generic_org_type=generic_org_type,
|
generic_org_type=generic_org_type,
|
||||||
is_election_board=is_election_board,
|
is_election_board=is_election_board,
|
||||||
|
@ -856,6 +860,8 @@ def completed_domain_request(
|
||||||
creator=user,
|
creator=user,
|
||||||
status=status,
|
status=status,
|
||||||
investigator=investigator,
|
investigator=investigator,
|
||||||
|
federal_agency=federal_agency,
|
||||||
|
updated_federal_agency=updated_federal_agency,
|
||||||
)
|
)
|
||||||
if has_about_your_organization:
|
if has_about_your_organization:
|
||||||
domain_request_kwargs["about_your_organization"] = "e-Government"
|
domain_request_kwargs["about_your_organization"] = "e-Government"
|
||||||
|
@ -864,7 +870,6 @@ def completed_domain_request(
|
||||||
|
|
||||||
if organization_type:
|
if organization_type:
|
||||||
domain_request_kwargs["organization_type"] = organization_type
|
domain_request_kwargs["organization_type"] = organization_type
|
||||||
|
|
||||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||||
|
|
||||||
if has_other_contacts:
|
if has_other_contacts:
|
||||||
|
|
|
@ -18,6 +18,7 @@ from registrar.admin import (
|
||||||
AuditedAdmin,
|
AuditedAdmin,
|
||||||
ContactAdmin,
|
ContactAdmin,
|
||||||
DomainInformationAdmin,
|
DomainInformationAdmin,
|
||||||
|
MyHostAdmin,
|
||||||
UserDomainRoleAdmin,
|
UserDomainRoleAdmin,
|
||||||
VerifiedByStaffAdmin,
|
VerifiedByStaffAdmin,
|
||||||
)
|
)
|
||||||
|
@ -76,6 +77,13 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
self.app.set_user(self.superuser.username)
|
self.app.set_user(self.superuser.username)
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
# Add domain data
|
||||||
|
self.ready_domain, _ = Domain.objects.get_or_create(name="fakeready.gov", state=Domain.State.READY)
|
||||||
|
self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN)
|
||||||
|
self.dns_domain, _ = Domain.objects.get_or_create(name="fakedns.gov", state=Domain.State.DNS_NEEDED)
|
||||||
|
self.hold_domain, _ = Domain.objects.get_or_create(name="fakehold.gov", state=Domain.State.ON_HOLD)
|
||||||
|
self.deleted_domain, _ = Domain.objects.get_or_create(name="fakedeleted.gov", state=Domain.State.DELETED)
|
||||||
|
|
||||||
# Contains some test tools
|
# Contains some test tools
|
||||||
self.test_helper = GenericTestHelper(
|
self.test_helper = GenericTestHelper(
|
||||||
factory=self.factory,
|
factory=self.factory,
|
||||||
|
@ -159,6 +167,68 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "usa-button__clipboard")
|
self.assertContains(response, "usa-button__clipboard")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_helper_text(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct helper text on this page
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
|
||||||
|
# These should exist in the response
|
||||||
|
expected_values = [
|
||||||
|
("expiration_date", "Date the domain expires in the registry"),
|
||||||
|
("first_ready_at", 'Date when this domain first moved into "ready" state; date will never change'),
|
||||||
|
("deleted_at", 'Will appear blank unless the domain is in "deleted" state'),
|
||||||
|
]
|
||||||
|
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_helper_text_state(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct state helper text on this page
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We don't need to check for all text content, just a portion of it
|
||||||
|
expected_unknown_domain_message = "The creator of the associated domain request has not logged in to"
|
||||||
|
expected_dns_message = "Before this domain can be used, name server addresses need"
|
||||||
|
expected_hold_message = "While on hold, this domain"
|
||||||
|
expected_deleted_message = "This domain was permanently removed from the registry."
|
||||||
|
expected_messages = [
|
||||||
|
(self.ready_domain, "This domain has name servers and is ready for use."),
|
||||||
|
(self.unknown_domain, expected_unknown_domain_message),
|
||||||
|
(self.dns_domain, expected_dns_message),
|
||||||
|
(self.hold_domain, expected_hold_message),
|
||||||
|
(self.deleted_domain, expected_deleted_message),
|
||||||
|
]
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
for domain, message in expected_messages:
|
||||||
|
with self.subTest(domain_state=domain.state):
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
|
||||||
|
# Check that the right help text exists
|
||||||
|
self.assertContains(response, message)
|
||||||
|
|
||||||
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
||||||
def test_extend_expiration_date_button(self, mock_date_today):
|
def test_extend_expiration_date_button(self, mock_date_today):
|
||||||
"""
|
"""
|
||||||
|
@ -782,6 +852,152 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
)
|
)
|
||||||
self.mock_client = MockSESClient()
|
self.mock_client = MockSESClient()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_helper_text(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct helper text on this page
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a fake domain request and domain
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain_request.requested_domain.name)
|
||||||
|
|
||||||
|
# These should exist in the response
|
||||||
|
expected_values = [
|
||||||
|
("creator", "Person who submitted the domain request; will not receive email updates"),
|
||||||
|
(
|
||||||
|
"submitter",
|
||||||
|
'Person listed under "your contact information" in the request form; will receive email updates',
|
||||||
|
),
|
||||||
|
("approved_domain", "Domain associated with this request; will be blank until request is approved"),
|
||||||
|
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
||||||
|
("alternative_domains", "Other domain names the creator provided for consideration"),
|
||||||
|
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
||||||
|
("Urbanization", "Required for Puerto Rico only"),
|
||||||
|
]
|
||||||
|
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_status_logs(self):
|
||||||
|
"""
|
||||||
|
Tests that the status changes are shown in a table on the domain request change form,
|
||||||
|
accurately and in chronological order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a fake domain request and domain
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED)
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain_request.requested_domain.name)
|
||||||
|
|
||||||
|
# Table will contain one row for Started
|
||||||
|
self.assertContains(response, "<td>Started</td>", count=1)
|
||||||
|
self.assertNotContains(response, "<td>Submitted</td>")
|
||||||
|
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Table will contain and extra row for Submitted
|
||||||
|
self.assertContains(response, "<td>Started</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>Submitted</td>", count=1)
|
||||||
|
|
||||||
|
domain_request.in_review()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Table will contain and extra row for In review
|
||||||
|
self.assertContains(response, "<td>Started</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>Submitted</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>In review</td>", count=1)
|
||||||
|
|
||||||
|
domain_request.action_needed()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Table will contain and extra row for Action needed
|
||||||
|
self.assertContains(response, "<td>Started</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>Submitted</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>In review</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>Action needed</td>", count=1)
|
||||||
|
|
||||||
|
domain_request.in_review()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define the expected sequence of status changes
|
||||||
|
expected_status_changes = [
|
||||||
|
"<td>In review</td>",
|
||||||
|
"<td>Action needed</td>",
|
||||||
|
"<td>In review</td>",
|
||||||
|
"<td>Submitted</td>",
|
||||||
|
"<td>Started</td>",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test for the order of status changes
|
||||||
|
for status_change in expected_status_changes:
|
||||||
|
self.assertContains(response, status_change, html=True)
|
||||||
|
|
||||||
|
# Table now contains 2 rows for Approved
|
||||||
|
self.assertContains(response, "<td>Started</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>Submitted</td>", count=1)
|
||||||
|
self.assertContains(response, "<td>In review</td>", count=2)
|
||||||
|
self.assertContains(response, "<td>Action needed</td>", count=1)
|
||||||
|
|
||||||
|
def test_collaspe_toggle_button_markup(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct collapse toggle button markup
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a fake domain request and domain
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain_request.requested_domain.name)
|
||||||
|
self.test_helper.assertContains(response, "<span>Show details</span>")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_analyst_can_see_and_edit_alternative_domain(self):
|
def test_analyst_can_see_and_edit_alternative_domain(self):
|
||||||
"""Tests if an analyst can still see and edit the alternative domain field"""
|
"""Tests if an analyst can still see and edit the alternative domain field"""
|
||||||
|
@ -1889,6 +2105,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"purpose",
|
"purpose",
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
|
"has_anything_else_text",
|
||||||
|
"cisa_representative_email",
|
||||||
|
"has_cisa_representative",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
"submission_date",
|
"submission_date",
|
||||||
"notes",
|
"notes",
|
||||||
|
@ -1920,6 +2139,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
@ -2315,6 +2535,54 @@ class DomainInvitationAdminTest(TestCase):
|
||||||
self.assertContains(response, retrieved_html, count=1)
|
self.assertContains(response, retrieved_html, count=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHostAdmin(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"""Setup environment for a mock admin user"""
|
||||||
|
super().setUp()
|
||||||
|
self.site = AdminSite()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.admin = MyHostAdmin(model=Host, admin_site=self.site)
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.test_helper = GenericTestHelper(
|
||||||
|
factory=self.factory,
|
||||||
|
user=self.superuser,
|
||||||
|
admin=self.admin,
|
||||||
|
url="/admin/registrar/Host/",
|
||||||
|
model=Host,
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
Host.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_helper_text(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct helper text on this page
|
||||||
|
"""
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
# Create a fake host
|
||||||
|
host, _ = Host.objects.get_or_create(name="ns1.test.gov", domain=domain)
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/host/{}/change/".format(host.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# These should exist in the response
|
||||||
|
expected_values = [
|
||||||
|
("domain", "Domain associated with this host"),
|
||||||
|
]
|
||||||
|
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainInformationAdmin(TestCase):
|
class TestDomainInformationAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Setup environment for a mock admin user"""
|
"""Setup environment for a mock admin user"""
|
||||||
|
@ -2367,6 +2635,38 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_helper_text(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct helper text on this page
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a fake domain request and domain
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
domain_request.approve()
|
||||||
|
domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domaininformation/{}/change/".format(domain_info.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded, and that we're on the right page
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain_info.domain.name)
|
||||||
|
|
||||||
|
# These should exist in the response
|
||||||
|
expected_values = [
|
||||||
|
("creator", "Person who submitted the domain request"),
|
||||||
|
("submitter", 'Person listed under "your contact information" in the request form'),
|
||||||
|
("domain_request", "Request associated with this domain"),
|
||||||
|
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
||||||
|
("urbanization", "Required for Puerto Rico only"),
|
||||||
|
]
|
||||||
|
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_other_contacts_has_readonly_link(self):
|
def test_other_contacts_has_readonly_link(self):
|
||||||
"""Tests if the readonly other_contacts field has links"""
|
"""Tests if the readonly other_contacts field has links"""
|
||||||
|
@ -2711,7 +3011,7 @@ class UserDomainRoleAdminTest(TestCase):
|
||||||
self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com</a></th>", count=1)
|
self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com</a></th>", count=1)
|
||||||
|
|
||||||
|
|
||||||
class ListHeaderAdminTest(TestCase):
|
class TestListHeaderAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
@ -2784,10 +3084,43 @@ class ListHeaderAdminTest(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdminTest(TestCase):
|
class TestMyUserAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
admin_site = AdminSite()
|
admin_site = AdminSite()
|
||||||
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.test_helper = GenericTestHelper(admin=self.admin)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_helper_text(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct helper text on this page
|
||||||
|
"""
|
||||||
|
user = create_user()
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/user/{}/change/".format(user.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# These should exist in the response
|
||||||
|
expected_values = [
|
||||||
|
("password", "Raw passwords are not stored, so they will not display here."),
|
||||||
|
("status", 'Users in "restricted" status cannot make updates in the registrar or start a new request.'),
|
||||||
|
("is_staff", "Designates whether the user can log in to this admin site"),
|
||||||
|
("is_superuser", "For development purposes only; provides superuser access on the database level"),
|
||||||
|
]
|
||||||
|
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
||||||
|
|
||||||
def test_list_display_without_username(self):
|
def test_list_display_without_username(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
|
@ -2809,8 +3142,9 @@ class MyUserAdminTest(TestCase):
|
||||||
def test_get_fieldsets_superuser(self):
|
def test_get_fieldsets_superuser(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
request = self.client.request().wsgi_request
|
request = self.client.request().wsgi_request
|
||||||
request.user = create_superuser()
|
request.user = self.superuser
|
||||||
fieldsets = self.admin.get_fieldsets(request)
|
fieldsets = self.admin.get_fieldsets(request)
|
||||||
|
|
||||||
expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request)
|
expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request)
|
||||||
self.assertEqual(fieldsets, expected_fieldsets)
|
self.assertEqual(fieldsets, expected_fieldsets)
|
||||||
|
|
||||||
|
@ -2820,16 +3154,21 @@ class MyUserAdminTest(TestCase):
|
||||||
request.user = create_user()
|
request.user = create_user()
|
||||||
fieldsets = self.admin.get_fieldsets(request)
|
fieldsets = self.admin.get_fieldsets(request)
|
||||||
expected_fieldsets = (
|
expected_fieldsets = (
|
||||||
(None, {"fields": ("password", "status")}),
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"status",
|
||||||
|
"verification_type",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
||||||
("Permissions", {"fields": ("is_active", "groups")}),
|
("Permissions", {"fields": ("is_active", "groups")}),
|
||||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
)
|
)
|
||||||
self.assertEqual(fieldsets, expected_fieldsets)
|
self.assertEqual(fieldsets, expected_fieldsets)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
User.objects.all().delete()
|
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdminTest(TestCase):
|
class AuditedAdminTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -3303,10 +3642,43 @@ class ContactAdminTest(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
class VerifiedByStaffAdminTestCase(TestCase):
|
class TestVerifiedByStaffAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.site = AdminSite()
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
self.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=self.site)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.test_helper = GenericTestHelper(admin=self.admin)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
VerifiedByStaff.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_helper_text(self):
|
||||||
|
"""
|
||||||
|
Tests for the correct helper text on this page
|
||||||
|
"""
|
||||||
|
vip_instance, _ = VerifiedByStaff.objects.get_or_create(email="test@example.com", notes="Test Notes")
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/verifiedbystaff/{}/change/".format(vip_instance.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# These should exist in the response
|
||||||
|
expected_values = [
|
||||||
|
("requestor", "Person who verified this user"),
|
||||||
|
]
|
||||||
|
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
||||||
|
|
||||||
def test_save_model_sets_user_field(self):
|
def test_save_model_sets_user_field(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
|
|
|
@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import (
|
||||||
RequirementsForm,
|
RequirementsForm,
|
||||||
TribalGovernmentForm,
|
TribalGovernmentForm,
|
||||||
PurposeForm,
|
PurposeForm,
|
||||||
AnythingElseForm,
|
AdditionalDetailsForm,
|
||||||
AboutYourOrganizationForm,
|
AboutYourOrganizationForm,
|
||||||
)
|
)
|
||||||
from registrar.forms.domain import ContactForm
|
from registrar.forms.domain import ContactForm
|
||||||
|
@ -274,7 +274,7 @@ class TestFormValidation(MockEppLib):
|
||||||
|
|
||||||
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
||||||
"""Response must be less than 2000 characters."""
|
"""Response must be less than 2000 characters."""
|
||||||
form = AnythingElseForm(
|
form = AdditionalDetailsForm(
|
||||||
data={
|
data={
|
||||||
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||||
|
|
|
@ -14,8 +14,9 @@ from registrar.models import (
|
||||||
TransitionDomain,
|
TransitionDomain,
|
||||||
DomainInformation,
|
DomainInformation,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
|
VerifiedByStaff,
|
||||||
|
PublicContact,
|
||||||
)
|
)
|
||||||
from registrar.models.public_contact import PublicContact
|
|
||||||
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
|
@ -25,6 +26,103 @@ from .common import MockEppLib, less_console_noise, completed_domain_request
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
|
||||||
|
|
||||||
|
class TestPopulateVerificationType(MockEppLib):
|
||||||
|
"""Tests for the populate_organization_type script"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Creates a fake domain object"""
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
# Get the domain requests
|
||||||
|
self.domain_request_1 = completed_domain_request(
|
||||||
|
name="lasers.gov",
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
is_election_board=True,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Approve the request
|
||||||
|
self.domain_request_1.approve()
|
||||||
|
|
||||||
|
# Get the domains
|
||||||
|
self.domain_1 = Domain.objects.get(name="lasers.gov")
|
||||||
|
|
||||||
|
# Get users
|
||||||
|
self.regular_user, _ = User.objects.get_or_create(username="testuser@igormail.gov")
|
||||||
|
|
||||||
|
vip, _ = VerifiedByStaff.objects.get_or_create(email="vipuser@igormail.gov")
|
||||||
|
self.verified_by_staff_user, _ = User.objects.get_or_create(username="vipuser@igormail.gov")
|
||||||
|
|
||||||
|
grandfathered, _ = TransitionDomain.objects.get_or_create(
|
||||||
|
username="grandpa@igormail.gov", domain_name=self.domain_1.name
|
||||||
|
)
|
||||||
|
self.grandfathered_user, _ = User.objects.get_or_create(username="grandpa@igormail.gov")
|
||||||
|
|
||||||
|
invited, _ = DomainInvitation.objects.get_or_create(
|
||||||
|
email="invited@igormail.gov", domain=self.domain_1, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
|
||||||
|
)
|
||||||
|
self.invited_user, _ = User.objects.get_or_create(username="invited@igormail.gov")
|
||||||
|
|
||||||
|
self.untouched_user, _ = User.objects.get_or_create(
|
||||||
|
username="iaminvincible@igormail.gov", verification_type=User.VerificationTypeChoices.GRANDFATHERED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fixture users should be untouched by the script. These will auto update once the
|
||||||
|
# user logs in / creates an account.
|
||||||
|
self.fixture_user, _ = User.objects.get_or_create(
|
||||||
|
username="fixture@igormail.gov", verification_type=User.VerificationTypeChoices.FIXTURE_USER
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Deletes all DB objects related to migrations"""
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
# Delete domains and related information
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
Website.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def run_populate_verification_type(self):
|
||||||
|
"""
|
||||||
|
This method executes the populate_organization_type command.
|
||||||
|
|
||||||
|
The 'call_command' function from Django's management framework is then used to
|
||||||
|
execute the populate_organization_type command with the specified arguments.
|
||||||
|
"""
|
||||||
|
with patch(
|
||||||
|
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
call_command("populate_verification_type")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_verification_type_script_populates_data(self):
|
||||||
|
"""Ensures that the verification type script actually populates data"""
|
||||||
|
|
||||||
|
# Run the script
|
||||||
|
self.run_populate_verification_type()
|
||||||
|
|
||||||
|
# Scripts don't work as we'd expect in our test environment, we need to manually
|
||||||
|
# trigger the refresh event
|
||||||
|
self.regular_user.refresh_from_db()
|
||||||
|
self.grandfathered_user.refresh_from_db()
|
||||||
|
self.invited_user.refresh_from_db()
|
||||||
|
self.verified_by_staff_user.refresh_from_db()
|
||||||
|
self.untouched_user.refresh_from_db()
|
||||||
|
|
||||||
|
# Test all users
|
||||||
|
self.assertEqual(self.regular_user.verification_type, User.VerificationTypeChoices.REGULAR)
|
||||||
|
self.assertEqual(self.grandfathered_user.verification_type, User.VerificationTypeChoices.GRANDFATHERED)
|
||||||
|
self.assertEqual(self.invited_user.verification_type, User.VerificationTypeChoices.INVITED)
|
||||||
|
self.assertEqual(self.verified_by_staff_user.verification_type, User.VerificationTypeChoices.VERIFIED_BY_STAFF)
|
||||||
|
self.assertEqual(self.untouched_user.verification_type, User.VerificationTypeChoices.GRANDFATHERED)
|
||||||
|
self.assertEqual(self.fixture_user.verification_type, User.VerificationTypeChoices.FIXTURE_USER)
|
||||||
|
|
||||||
|
|
||||||
class TestPopulateOrganizationType(MockEppLib):
|
class TestPopulateOrganizationType(MockEppLib):
|
||||||
"""Tests for the populate_organization_type script"""
|
"""Tests for the populate_organization_type script"""
|
||||||
|
|
||||||
|
@ -743,3 +841,120 @@ class TestDiscloseEmails(MockEppLib):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO in #1793: Remove this whole test class
|
||||||
|
class TestPopulateDomainUpdatedFederalAgency(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
# Get the domain requests
|
||||||
|
self.domain_request_1 = completed_domain_request(
|
||||||
|
name="stitches.gov",
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
is_election_board=True,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
federal_agency="U.S. Peace Corps",
|
||||||
|
)
|
||||||
|
self.domain_request_2 = completed_domain_request(
|
||||||
|
name="fadoesntexist.gov",
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
is_election_board=True,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
federal_agency="MEOWARDRULES",
|
||||||
|
)
|
||||||
|
self.domain_request_3 = completed_domain_request(
|
||||||
|
name="nullfederalagency.gov",
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
is_election_board=True,
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
federal_agency=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Approve all three requests
|
||||||
|
self.domain_request_1.approve()
|
||||||
|
self.domain_request_2.approve()
|
||||||
|
self.domain_request_3.approve()
|
||||||
|
|
||||||
|
# Get the domains
|
||||||
|
self.domain_1 = Domain.objects.get(name="stitches.gov")
|
||||||
|
self.domain_2 = Domain.objects.get(name="fadoesntexist.gov")
|
||||||
|
self.domain_3 = Domain.objects.get(name="nullfederalagency.gov")
|
||||||
|
|
||||||
|
# Get the domain infos
|
||||||
|
self.domain_info_1 = DomainInformation.objects.get(domain=self.domain_1)
|
||||||
|
self.domain_info_2 = DomainInformation.objects.get(domain=self.domain_2)
|
||||||
|
self.domain_info_3 = DomainInformation.objects.get(domain=self.domain_3)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
|
||||||
|
def run_populate_domain_updated_federal_agency(self):
|
||||||
|
"""
|
||||||
|
This method executes the populate_domain_updated_federal_agency command.
|
||||||
|
|
||||||
|
The 'call_command' function from Django's management framework is then used to
|
||||||
|
execute the populate_domain_updated_federal_agency command.
|
||||||
|
"""
|
||||||
|
with less_console_noise():
|
||||||
|
call_command("populate_domain_updated_federal_agency")
|
||||||
|
|
||||||
|
def test_domain_information_renaming_federal_agency_success(self):
|
||||||
|
"""
|
||||||
|
Domain Information updates successfully for an "outdated" Federal Agency
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.run_populate_domain_updated_federal_agency()
|
||||||
|
|
||||||
|
self.domain_info_1.refresh_from_db()
|
||||||
|
|
||||||
|
previous_federal_agency_name = self.domain_info_1.federal_agency
|
||||||
|
|
||||||
|
updated_federal_agency_name = self.domain_info_1.updated_federal_agency.agency
|
||||||
|
|
||||||
|
self.assertEqual(previous_federal_agency_name, "U.S. Peace Corps")
|
||||||
|
self.assertEqual(updated_federal_agency_name, "Peace Corps")
|
||||||
|
|
||||||
|
def test_domain_information_does_not_exist(self):
|
||||||
|
"""
|
||||||
|
Update a Federal Agency that doesn't exist
|
||||||
|
(should return None bc the Federal Agency didn't exist before)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.run_populate_domain_updated_federal_agency()
|
||||||
|
|
||||||
|
self.domain_info_2.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(self.domain_info_2.updated_federal_agency, None)
|
||||||
|
|
||||||
|
def test_domain_request_is_skipped(self):
|
||||||
|
"""
|
||||||
|
Update a Domain Request that doesn't exist
|
||||||
|
(should return None bc the Federal Agency didn't exist before)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Test case #2
|
||||||
|
self.run_populate_domain_updated_federal_agency()
|
||||||
|
|
||||||
|
self.domain_request_2.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(self.domain_request_2.updated_federal_agency, None)
|
||||||
|
|
||||||
|
def test_domain_information_updating_null_federal_agency_to_non_federal_agency(self):
|
||||||
|
"""
|
||||||
|
Updating a Domain Information that was previously None
|
||||||
|
to Non-Federal Agency
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.run_populate_domain_updated_federal_agency()
|
||||||
|
|
||||||
|
self.domain_info_3.refresh_from_db()
|
||||||
|
|
||||||
|
previous_federal_agency_name = self.domain_info_3.federal_agency
|
||||||
|
updated_federal_agency_name = self.domain_info_3.updated_federal_agency.agency
|
||||||
|
|
||||||
|
self.assertEqual(previous_federal_agency_name, None)
|
||||||
|
self.assertEqual(updated_federal_agency_name, "Non-Federal Agency")
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
from datetime import date
|
||||||
from django.test import Client, TestCase, override_settings
|
from django.test import Client, TestCase, override_settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
from registrar.models.contact import Contact
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.draft_domain import DraftDomain
|
||||||
|
from registrar.models.user import User
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from registrar.views.domain import DomainNameserversView
|
from registrar.views.domain import DomainNameserversView
|
||||||
|
|
||||||
from .common import MockEppLib # type: ignore
|
from .common import MockEppLib, less_console_noise # type: ignore
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@ -135,3 +139,369 @@ class TestEnvironmentVariablesEffects(TestCase):
|
||||||
self.assertEqual(contact_page_500.status_code, 500)
|
self.assertEqual(contact_page_500.status_code, 500)
|
||||||
|
|
||||||
self.assertNotContains(contact_page_500, "You are on a test site.")
|
self.assertNotContains(contact_page_500, "You are on a test site.")
|
||||||
|
|
||||||
|
|
||||||
|
class HomeTests(TestWithUser):
|
||||||
|
"""A series of tests that target the two tables on home.html"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
|
def test_empty_domain_table(self):
|
||||||
|
response = self.client.get("/")
|
||||||
|
self.assertContains(response, "You don't have any registered domains.")
|
||||||
|
self.assertContains(response, "Why don't I see my domain when I sign in to the registrar?")
|
||||||
|
|
||||||
|
def test_home_lists_domain_requests(self):
|
||||||
|
response = self.client.get("/")
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
domain_request = DomainRequest.objects.create(creator=self.user, requested_domain=site)
|
||||||
|
response = self.client.get("/")
|
||||||
|
|
||||||
|
# count = 7 because of screenreader content
|
||||||
|
self.assertContains(response, "igorville.gov", count=7)
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
domain_request.delete()
|
||||||
|
|
||||||
|
def test_state_help_text(self):
|
||||||
|
"""Tests if each domain state has help text"""
|
||||||
|
|
||||||
|
# Get the expected text content of each state
|
||||||
|
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
||||||
|
dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses."
|
||||||
|
ready_text = "This domain has name servers and is ready for use."
|
||||||
|
on_hold_text = (
|
||||||
|
"This domain is administratively paused, "
|
||||||
|
"so it can’t be edited and won’t resolve in DNS. "
|
||||||
|
"Contact help@get.gov for details."
|
||||||
|
)
|
||||||
|
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
||||||
|
# Generate a mapping of domain names, the state, and expected messages for the subtest
|
||||||
|
test_cases = [
|
||||||
|
("deleted.gov", Domain.State.DELETED, deleted_text),
|
||||||
|
("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text),
|
||||||
|
("unknown.gov", Domain.State.UNKNOWN, dns_needed_text),
|
||||||
|
("onhold.gov", Domain.State.ON_HOLD, on_hold_text),
|
||||||
|
("ready.gov", Domain.State.READY, ready_text),
|
||||||
|
]
|
||||||
|
for domain_name, state, expected_message in test_cases:
|
||||||
|
with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message):
|
||||||
|
# Create a domain and a UserRole with the given params
|
||||||
|
test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state)
|
||||||
|
test_domain.expiration_date = date.today()
|
||||||
|
test_domain.save()
|
||||||
|
|
||||||
|
user_role, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
# Grab the home page
|
||||||
|
response = self.client.get("/")
|
||||||
|
|
||||||
|
# Make sure the user can actually see the domain.
|
||||||
|
# We expect two instances because of SR content.
|
||||||
|
self.assertContains(response, domain_name, count=2)
|
||||||
|
|
||||||
|
# Check that we have the right text content.
|
||||||
|
self.assertContains(response, expected_message, count=1)
|
||||||
|
|
||||||
|
# Delete the role and domain to ensure we're testing in isolation
|
||||||
|
user_role.delete()
|
||||||
|
test_domain.delete()
|
||||||
|
|
||||||
|
def test_state_help_text_expired(self):
|
||||||
|
"""Tests if each domain state has help text when expired"""
|
||||||
|
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
||||||
|
test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY)
|
||||||
|
test_domain.expiration_date = date(2011, 10, 10)
|
||||||
|
test_domain.save()
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
|
# Grab the home page
|
||||||
|
response = self.client.get("/")
|
||||||
|
|
||||||
|
# Make sure the user can actually see the domain.
|
||||||
|
# We expect two instances because of SR content.
|
||||||
|
self.assertContains(response, "expired.gov", count=2)
|
||||||
|
|
||||||
|
# Check that we have the right text content.
|
||||||
|
self.assertContains(response, expired_text, count=1)
|
||||||
|
|
||||||
|
def test_state_help_text_no_expiration_date(self):
|
||||||
|
"""Tests if each domain state has help text when expiration date is None"""
|
||||||
|
|
||||||
|
# == Test a expiration of None for state ready. This should be expired. == #
|
||||||
|
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
||||||
|
test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY)
|
||||||
|
test_domain.expiration_date = None
|
||||||
|
test_domain.save()
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
|
# Grab the home page
|
||||||
|
response = self.client.get("/")
|
||||||
|
|
||||||
|
# Make sure the user can actually see the domain.
|
||||||
|
# We expect two instances because of SR content.
|
||||||
|
self.assertContains(response, "imexpired.gov", count=2)
|
||||||
|
|
||||||
|
# Make sure the expiration date is None
|
||||||
|
self.assertEqual(test_domain.expiration_date, None)
|
||||||
|
|
||||||
|
# Check that we have the right text content.
|
||||||
|
self.assertContains(response, expired_text, count=1)
|
||||||
|
|
||||||
|
# == Test a expiration of None for state unknown. This should not display expired text. == #
|
||||||
|
unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses."
|
||||||
|
test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN)
|
||||||
|
test_domain_2.expiration_date = None
|
||||||
|
test_domain_2.save()
|
||||||
|
|
||||||
|
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
|
# Grab the home page
|
||||||
|
response = self.client.get("/")
|
||||||
|
|
||||||
|
# Make sure the user can actually see the domain.
|
||||||
|
# We expect two instances because of SR content.
|
||||||
|
self.assertContains(response, "notexpired.gov", count=2)
|
||||||
|
|
||||||
|
# Make sure the expiration date is None
|
||||||
|
self.assertEqual(test_domain_2.expiration_date, None)
|
||||||
|
|
||||||
|
# Check that we have the right text content.
|
||||||
|
self.assertContains(response, unknown_text, count=1)
|
||||||
|
|
||||||
|
def test_home_deletes_withdrawn_domain_request(self):
|
||||||
|
"""Tests if the user can delete a DomainRequest in the 'withdrawn' status"""
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
domain_request = DomainRequest.objects.create(
|
||||||
|
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.WITHDRAWN
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure that igorville.gov exists on the page
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "igorville.gov")
|
||||||
|
|
||||||
|
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||||
|
self.assertContains(home_page, "Delete")
|
||||||
|
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
domain_request.delete()
|
||||||
|
|
||||||
|
def test_home_deletes_started_domain_request(self):
|
||||||
|
"""Tests if the user can delete a DomainRequest in the 'started' status"""
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
domain_request = DomainRequest.objects.create(
|
||||||
|
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.STARTED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure that igorville.gov exists on the page
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "igorville.gov")
|
||||||
|
|
||||||
|
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||||
|
self.assertContains(home_page, "Delete")
|
||||||
|
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
domain_request.delete()
|
||||||
|
|
||||||
|
def test_home_doesnt_delete_other_domain_requests(self):
|
||||||
|
"""Tests to ensure the user can't delete domain requests not in the status of STARTED or WITHDRAWN"""
|
||||||
|
|
||||||
|
# Given that we are including a subset of items that can be deleted while excluding the rest,
|
||||||
|
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
|
||||||
|
with less_console_noise():
|
||||||
|
draft_domain = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
for status in DomainRequest.DomainRequestStatus:
|
||||||
|
if status not in [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
]:
|
||||||
|
with self.subTest(status=status):
|
||||||
|
domain_request = DomainRequest.objects.create(
|
||||||
|
creator=self.user, requested_domain=draft_domain, status=status
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for a 403 error - the end user should not be allowed to do this
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
desired_domain_request = DomainRequest.objects.filter(requested_domain=draft_domain)
|
||||||
|
|
||||||
|
# Make sure the DomainRequest wasn't deleted
|
||||||
|
self.assertEqual(desired_domain_request.count(), 1)
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
domain_request.delete()
|
||||||
|
|
||||||
|
def test_home_deletes_domain_request_and_orphans(self):
|
||||||
|
"""Tests if delete for DomainRequest deletes orphaned Contact objects"""
|
||||||
|
|
||||||
|
# Create the site and contacts to delete (orphaned)
|
||||||
|
contact = Contact.objects.create(
|
||||||
|
first_name="Henry",
|
||||||
|
last_name="Mcfakerson",
|
||||||
|
)
|
||||||
|
contact_shared = Contact.objects.create(
|
||||||
|
first_name="Relative",
|
||||||
|
last_name="Aether",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create two non-orphaned contacts
|
||||||
|
contact_2 = Contact.objects.create(
|
||||||
|
first_name="Saturn",
|
||||||
|
last_name="Mars",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach a user object to a contact (should not be deleted)
|
||||||
|
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
domain_request = DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site,
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
authorizing_official=contact,
|
||||||
|
submitter=contact_user,
|
||||||
|
)
|
||||||
|
domain_request.other_contacts.set([contact_2])
|
||||||
|
|
||||||
|
# Create a second domain request to attach contacts to
|
||||||
|
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||||
|
domain_request_2 = DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site_2,
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
authorizing_official=contact_2,
|
||||||
|
submitter=contact_shared,
|
||||||
|
)
|
||||||
|
domain_request_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
|
# Ensure that igorville.gov exists on the page
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "igorville.gov")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
# igorville is now deleted
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
|
||||||
|
# Check if the orphaned contact was deleted
|
||||||
|
orphan = Contact.objects.filter(id=contact.id)
|
||||||
|
self.assertFalse(orphan.exists())
|
||||||
|
|
||||||
|
# All non-orphan contacts should still exist and are unaltered
|
||||||
|
try:
|
||||||
|
current_user = Contact.objects.filter(id=contact_user.id).get()
|
||||||
|
except Contact.DoesNotExist:
|
||||||
|
self.fail("contact_user (a non-orphaned contact) was deleted")
|
||||||
|
|
||||||
|
self.assertEqual(current_user, contact_user)
|
||||||
|
try:
|
||||||
|
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||||
|
except Contact.DoesNotExist:
|
||||||
|
self.fail("contact_2 (a non-orphaned contact) was deleted")
|
||||||
|
|
||||||
|
self.assertEqual(edge_case, contact_2)
|
||||||
|
|
||||||
|
def test_home_deletes_domain_request_and_shared_orphans(self):
|
||||||
|
"""Test the edge case for an object that will become orphaned after a delete
|
||||||
|
(but is not an orphan at the time of deletion)"""
|
||||||
|
|
||||||
|
# Create the site and contacts to delete (orphaned)
|
||||||
|
contact = Contact.objects.create(
|
||||||
|
first_name="Henry",
|
||||||
|
last_name="Mcfakerson",
|
||||||
|
)
|
||||||
|
contact_shared = Contact.objects.create(
|
||||||
|
first_name="Relative",
|
||||||
|
last_name="Aether",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create two non-orphaned contacts
|
||||||
|
contact_2 = Contact.objects.create(
|
||||||
|
first_name="Saturn",
|
||||||
|
last_name="Mars",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach a user object to a contact (should not be deleted)
|
||||||
|
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
domain_request = DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site,
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
authorizing_official=contact,
|
||||||
|
submitter=contact_user,
|
||||||
|
)
|
||||||
|
domain_request.other_contacts.set([contact_2])
|
||||||
|
|
||||||
|
# Create a second domain request to attach contacts to
|
||||||
|
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||||
|
domain_request_2 = DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site_2,
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
authorizing_official=contact_2,
|
||||||
|
submitter=contact_shared,
|
||||||
|
)
|
||||||
|
domain_request_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "teaville.gov")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request_2.pk}), follow=True)
|
||||||
|
|
||||||
|
self.assertNotContains(response, "teaville.gov")
|
||||||
|
|
||||||
|
# Check if the orphaned contact was deleted
|
||||||
|
orphan = Contact.objects.filter(id=contact_shared.id)
|
||||||
|
self.assertFalse(orphan.exists())
|
||||||
|
|
||||||
|
def test_domain_request_form_view(self):
|
||||||
|
response = self.client.get("/request/", follow=True)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"You’re about to start your .gov domain request.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_domain_request_form_with_ineligible_user(self):
|
||||||
|
"""Domain request form not accessible for an ineligible user.
|
||||||
|
This test should be solid enough since all domain request wizard
|
||||||
|
views share the same permissions class"""
|
||||||
|
self.user.status = User.RESTRICTED
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
response = self.client.get("/request/", follow=True)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
|
@ -344,8 +344,6 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Ensure that the user has its original permissions"""
|
"""Ensure that the user has its original permissions"""
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
self.user.is_staff = False
|
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
def test_domain_managers(self):
|
def test_domain_managers(self):
|
||||||
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||||
|
|
|
@ -3,7 +3,6 @@ from unittest.mock import Mock
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from .common import MockSESClient, completed_domain_request # type: ignore
|
from .common import MockSESClient, completed_domain_request # type: ignore
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
|
@ -17,7 +16,6 @@ from registrar.models import (
|
||||||
Contact,
|
Contact,
|
||||||
User,
|
User,
|
||||||
Website,
|
Website,
|
||||||
UserDomainRole,
|
|
||||||
)
|
)
|
||||||
from registrar.views.domain_request import DomainRequestWizard, Step
|
from registrar.views.domain_request import DomainRequestWizard, Step
|
||||||
|
|
||||||
|
@ -356,33 +354,39 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
# the domain request page
|
# the domain request page
|
||||||
self.assertEqual(other_contacts_result.status_code, 302)
|
self.assertEqual(other_contacts_result.status_code, 302)
|
||||||
self.assertEqual(other_contacts_result["Location"], "/request/anything_else/")
|
self.assertEqual(other_contacts_result["Location"], "/request/additional_details/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
|
||||||
# ---- ANYTHING ELSE PAGE ----
|
# ---- ADDITIONAL DETAILS PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
anything_else_page = other_contacts_result.follow()
|
additional_details_page = other_contacts_result.follow()
|
||||||
anything_else_form = anything_else_page.forms[0]
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
anything_else_form["anything_else-anything_else"] = "Nothing else."
|
# load inputs with test data
|
||||||
|
|
||||||
|
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||||
|
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||||
|
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
||||||
|
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
||||||
|
|
||||||
# test next button
|
# test next button
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
anything_else_result = anything_else_form.submit()
|
additional_details_result = additional_details_form.submit()
|
||||||
# validate that data from this step are being saved
|
# validate that data from this step are being saved
|
||||||
domain_request = DomainRequest.objects.get() # there's only one
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
||||||
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
# the domain request page
|
# the domain request page
|
||||||
self.assertEqual(anything_else_result.status_code, 302)
|
self.assertEqual(additional_details_result.status_code, 302)
|
||||||
self.assertEqual(anything_else_result["Location"], "/request/requirements/")
|
self.assertEqual(additional_details_result["Location"], "/request/requirements/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
|
||||||
# ---- REQUIREMENTS PAGE ----
|
# ---- REQUIREMENTS PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
requirements_page = anything_else_result.follow()
|
requirements_page = additional_details_result.follow()
|
||||||
requirements_form = requirements_page.forms[0]
|
requirements_form = requirements_page.forms[0]
|
||||||
|
|
||||||
requirements_form["requirements-is_policy_acknowledged"] = True
|
requirements_form["requirements-is_policy_acknowledged"] = True
|
||||||
|
@ -434,6 +438,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
self.assertContains(review_page, "Another Tester")
|
self.assertContains(review_page, "Another Tester")
|
||||||
self.assertContains(review_page, "testy2@town.com")
|
self.assertContains(review_page, "testy2@town.com")
|
||||||
self.assertContains(review_page, "(201) 555-5557")
|
self.assertContains(review_page, "(201) 555-5557")
|
||||||
|
self.assertContains(review_page, "FakeEmail@gmail.com")
|
||||||
self.assertContains(review_page, "Nothing else.")
|
self.assertContains(review_page, "Nothing else.")
|
||||||
|
|
||||||
# We can't test the modal itself as it relies on JS for init and triggering,
|
# We can't test the modal itself as it relies on JS for init and triggering,
|
||||||
|
@ -717,13 +722,25 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
|
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
|
||||||
|
|
||||||
def test_yes_no_form_inits_blank_for_new_domain_request(self):
|
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
|
||||||
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
||||||
new domain requests"""
|
new domain requests"""
|
||||||
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
|
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
|
||||||
other_contacts_form = other_contacts_page.forms[0]
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
|
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
|
||||||
|
|
||||||
|
def test_yes_no_additional_form_inits_blank_for_new_domain_request(self):
|
||||||
|
"""On the Additional Details page, the yes/no form gets initialized with nothing selected for
|
||||||
|
new domain requests"""
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
additional_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Check the cisa representative yes/no field
|
||||||
|
self.assertEquals(additional_form["additional_details-has_cisa_representative"].value, None)
|
||||||
|
|
||||||
|
# Check the anything else yes/no field
|
||||||
|
self.assertEquals(additional_form["additional_details-has_anything_else_text"].value, None)
|
||||||
|
|
||||||
def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self):
|
def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self):
|
||||||
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
|
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
|
||||||
domain request has other contacts"""
|
domain request has other contacts"""
|
||||||
|
@ -744,6 +761,38 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
other_contacts_form = other_contacts_page.forms[0]
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
|
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
|
||||||
|
|
||||||
|
def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self):
|
||||||
|
"""On the Additional Details page, the yes/no form gets initialized with YES selected
|
||||||
|
for both yes/no radios if the domain request has a value for cisa_representative and
|
||||||
|
anything_else"""
|
||||||
|
|
||||||
|
domain_request = completed_domain_request(user=self.user, has_anything_else=True)
|
||||||
|
domain_request.cisa_representative_email = "test@igorville.gov"
|
||||||
|
domain_request.anything_else = "1234"
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Check the cisa representative yes/no field
|
||||||
|
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
||||||
|
self.assertEquals(yes_no_cisa, "True")
|
||||||
|
|
||||||
|
# Check the anything else yes/no field
|
||||||
|
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
||||||
|
self.assertEquals(yes_no_anything_else, "True")
|
||||||
|
|
||||||
def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self):
|
def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self):
|
||||||
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
|
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
|
||||||
domain request has no other contacts"""
|
domain request has no other contacts"""
|
||||||
|
@ -766,6 +815,230 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
other_contacts_form = other_contacts_page.forms[0]
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
|
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
|
||||||
|
|
||||||
|
def test_yes_no_form_for_domain_request_with_no_cisa_representative_and_anything_else(self):
|
||||||
|
"""On the Additional details page, the form preselects "no" when has_cisa_representative
|
||||||
|
and anything_else is no"""
|
||||||
|
|
||||||
|
domain_request = completed_domain_request(user=self.user, has_anything_else=False)
|
||||||
|
|
||||||
|
# Unlike the other contacts form, the no button is tracked with these boolean fields.
|
||||||
|
# This means that we should expect this to correlate with the no button.
|
||||||
|
domain_request.has_anything_else_text = False
|
||||||
|
domain_request.has_cisa_representative = False
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Check the cisa representative yes/no field
|
||||||
|
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
||||||
|
self.assertEquals(yes_no_cisa, "False")
|
||||||
|
|
||||||
|
# Check the anything else yes/no field
|
||||||
|
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
||||||
|
self.assertEquals(yes_no_anything_else, "False")
|
||||||
|
|
||||||
|
def test_submitting_additional_details_deletes_cisa_representative_and_anything_else(self):
|
||||||
|
"""When a user submits the Additional Details form with no selected for all fields,
|
||||||
|
the domain request's data gets wiped when submitted"""
|
||||||
|
domain_request = completed_domain_request(name="nocisareps.gov", user=self.user)
|
||||||
|
domain_request.cisa_representative_email = "fake@faketown.gov"
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
# Make sure we have the data we need for the test
|
||||||
|
self.assertEqual(domain_request.anything_else, "There is more")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov")
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Check the cisa representative yes/no field
|
||||||
|
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
||||||
|
self.assertEquals(yes_no_cisa, "True")
|
||||||
|
|
||||||
|
# Check the anything else yes/no field
|
||||||
|
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
||||||
|
self.assertEquals(yes_no_anything_else, "True")
|
||||||
|
|
||||||
|
# Set fields to false
|
||||||
|
additional_details_form["additional_details-has_cisa_representative"] = "False"
|
||||||
|
additional_details_form["additional_details-has_anything_else_text"] = "False"
|
||||||
|
|
||||||
|
# Submit the form
|
||||||
|
additional_details_form.submit()
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Verify that the anything_else and cisa_representative have been deleted from the DB
|
||||||
|
domain_request = DomainRequest.objects.get(requested_domain__name="nocisareps.gov")
|
||||||
|
|
||||||
|
# Check that our data has been cleared
|
||||||
|
self.assertEqual(domain_request.anything_else, None)
|
||||||
|
self.assertEqual(domain_request.cisa_representative_email, None)
|
||||||
|
|
||||||
|
# Double check the yes/no fields
|
||||||
|
self.assertEqual(domain_request.has_anything_else_text, False)
|
||||||
|
self.assertEqual(domain_request.has_cisa_representative, False)
|
||||||
|
|
||||||
|
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
|
||||||
|
"""When a user submits the Additional Details form,
|
||||||
|
the domain request's data gets submitted"""
|
||||||
|
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||||
|
|
||||||
|
# Make sure we have the data we need for the test
|
||||||
|
self.assertEqual(domain_request.anything_else, None)
|
||||||
|
self.assertEqual(domain_request.cisa_representative_email, None)
|
||||||
|
|
||||||
|
# These fields should not be selected at all, since we haven't initialized the form yet
|
||||||
|
self.assertEqual(domain_request.has_anything_else_text, None)
|
||||||
|
self.assertEqual(domain_request.has_cisa_representative, None)
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Set fields to true, and set data on those fields
|
||||||
|
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||||
|
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||||
|
additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov"
|
||||||
|
additional_details_form["additional_details-anything_else"] = "redandblue"
|
||||||
|
|
||||||
|
# Submit the form
|
||||||
|
additional_details_form.submit()
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Verify that the anything_else and cisa_representative exist in the db
|
||||||
|
domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov")
|
||||||
|
|
||||||
|
self.assertEqual(domain_request.anything_else, "redandblue")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_email, "test@faketest.gov")
|
||||||
|
|
||||||
|
self.assertEqual(domain_request.has_cisa_representative, True)
|
||||||
|
self.assertEqual(domain_request.has_anything_else_text, True)
|
||||||
|
|
||||||
|
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
|
||||||
|
"""Applicants with a cisa representative must provide a value"""
|
||||||
|
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Set fields to true, and set data on those fields
|
||||||
|
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||||
|
additional_details_form["additional_details-has_anything_else_text"] = "False"
|
||||||
|
|
||||||
|
# Submit the form
|
||||||
|
response = additional_details_form.submit()
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
self.assertContains(response, "Enter the email address of your CISA regional representative.")
|
||||||
|
|
||||||
|
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
|
||||||
|
"""Applicants with a anything else must provide a value"""
|
||||||
|
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Set fields to true, and set data on those fields
|
||||||
|
additional_details_form["additional_details-has_cisa_representative"] = "False"
|
||||||
|
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||||
|
|
||||||
|
# Submit the form
|
||||||
|
response = additional_details_form.submit()
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
expected_message = "Provide additional details you’d like us to know. If you have nothing to add, select “No.”"
|
||||||
|
self.assertContains(response, expected_message)
|
||||||
|
|
||||||
|
def test_additional_details_form_fields_required(self):
|
||||||
|
"""When a user submits the Additional Details form without checking the
|
||||||
|
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
|
||||||
|
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
||||||
|
|
||||||
|
self.assertEqual(domain_request.has_anything_else_text, None)
|
||||||
|
self.assertEqual(domain_request.has_cisa_representative, None)
|
||||||
|
|
||||||
|
# prime the form by visiting /edit
|
||||||
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_page = self.app.get(reverse("domain-request:additional_details"))
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# Submit the form
|
||||||
|
response = additional_details_form.submit()
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# We expect to see this twice for both fields. This results in a count of 4
|
||||||
|
# due to screen reader information / html.
|
||||||
|
self.assertContains(response, "This question is required.", count=4)
|
||||||
|
|
||||||
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
|
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
|
||||||
"""When a user submits the Other Contacts form with other contacts selected, the domain request's
|
"""When a user submits the Other Contacts form with other contacts selected, the domain request's
|
||||||
no other contacts rationale gets deleted"""
|
no other contacts rationale gets deleted"""
|
||||||
|
@ -2328,364 +2601,3 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.fail(f"Expected a redirect, but got a different response: {response}")
|
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||||
|
|
||||||
|
|
||||||
class HomeTests(TestWithUser):
|
|
||||||
"""A series of tests that target the two tables on home.html"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super().tearDown()
|
|
||||||
Contact.objects.all().delete()
|
|
||||||
|
|
||||||
def test_home_lists_domain_requests(self):
|
|
||||||
response = self.client.get("/")
|
|
||||||
self.assertNotContains(response, "igorville.gov")
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
domain_request = DomainRequest.objects.create(creator=self.user, requested_domain=site)
|
|
||||||
response = self.client.get("/")
|
|
||||||
|
|
||||||
# count = 7 because of screenreader content
|
|
||||||
self.assertContains(response, "igorville.gov", count=7)
|
|
||||||
|
|
||||||
# clean up
|
|
||||||
domain_request.delete()
|
|
||||||
|
|
||||||
def test_state_help_text(self):
|
|
||||||
"""Tests if each domain state has help text"""
|
|
||||||
|
|
||||||
# Get the expected text content of each state
|
|
||||||
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
|
||||||
dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses."
|
|
||||||
ready_text = "This domain has name servers and is ready for use."
|
|
||||||
on_hold_text = (
|
|
||||||
"This domain is administratively paused, "
|
|
||||||
"so it can’t be edited and won’t resolve in DNS. "
|
|
||||||
"Contact help@get.gov for details."
|
|
||||||
)
|
|
||||||
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
|
||||||
# Generate a mapping of domain names, the state, and expected messages for the subtest
|
|
||||||
test_cases = [
|
|
||||||
("deleted.gov", Domain.State.DELETED, deleted_text),
|
|
||||||
("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text),
|
|
||||||
("unknown.gov", Domain.State.UNKNOWN, dns_needed_text),
|
|
||||||
("onhold.gov", Domain.State.ON_HOLD, on_hold_text),
|
|
||||||
("ready.gov", Domain.State.READY, ready_text),
|
|
||||||
]
|
|
||||||
for domain_name, state, expected_message in test_cases:
|
|
||||||
with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message):
|
|
||||||
# Create a domain and a UserRole with the given params
|
|
||||||
test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state)
|
|
||||||
test_domain.expiration_date = date.today()
|
|
||||||
test_domain.save()
|
|
||||||
|
|
||||||
user_role, _ = UserDomainRole.objects.get_or_create(
|
|
||||||
user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
# Grab the home page
|
|
||||||
response = self.client.get("/")
|
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
|
||||||
# We expect two instances because of SR content.
|
|
||||||
self.assertContains(response, domain_name, count=2)
|
|
||||||
|
|
||||||
# Check that we have the right text content.
|
|
||||||
self.assertContains(response, expected_message, count=1)
|
|
||||||
|
|
||||||
# Delete the role and domain to ensure we're testing in isolation
|
|
||||||
user_role.delete()
|
|
||||||
test_domain.delete()
|
|
||||||
|
|
||||||
def test_state_help_text_expired(self):
|
|
||||||
"""Tests if each domain state has help text when expired"""
|
|
||||||
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
|
||||||
test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY)
|
|
||||||
test_domain.expiration_date = date(2011, 10, 10)
|
|
||||||
test_domain.save()
|
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
|
||||||
|
|
||||||
# Grab the home page
|
|
||||||
response = self.client.get("/")
|
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
|
||||||
# We expect two instances because of SR content.
|
|
||||||
self.assertContains(response, "expired.gov", count=2)
|
|
||||||
|
|
||||||
# Check that we have the right text content.
|
|
||||||
self.assertContains(response, expired_text, count=1)
|
|
||||||
|
|
||||||
def test_state_help_text_no_expiration_date(self):
|
|
||||||
"""Tests if each domain state has help text when expiration date is None"""
|
|
||||||
|
|
||||||
# == Test a expiration of None for state ready. This should be expired. == #
|
|
||||||
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
|
||||||
test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY)
|
|
||||||
test_domain.expiration_date = None
|
|
||||||
test_domain.save()
|
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
|
||||||
|
|
||||||
# Grab the home page
|
|
||||||
response = self.client.get("/")
|
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
|
||||||
# We expect two instances because of SR content.
|
|
||||||
self.assertContains(response, "imexpired.gov", count=2)
|
|
||||||
|
|
||||||
# Make sure the expiration date is None
|
|
||||||
self.assertEqual(test_domain.expiration_date, None)
|
|
||||||
|
|
||||||
# Check that we have the right text content.
|
|
||||||
self.assertContains(response, expired_text, count=1)
|
|
||||||
|
|
||||||
# == Test a expiration of None for state unknown. This should not display expired text. == #
|
|
||||||
unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses."
|
|
||||||
test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN)
|
|
||||||
test_domain_2.expiration_date = None
|
|
||||||
test_domain_2.save()
|
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER)
|
|
||||||
|
|
||||||
# Grab the home page
|
|
||||||
response = self.client.get("/")
|
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
|
||||||
# We expect two instances because of SR content.
|
|
||||||
self.assertContains(response, "notexpired.gov", count=2)
|
|
||||||
|
|
||||||
# Make sure the expiration date is None
|
|
||||||
self.assertEqual(test_domain_2.expiration_date, None)
|
|
||||||
|
|
||||||
# Check that we have the right text content.
|
|
||||||
self.assertContains(response, unknown_text, count=1)
|
|
||||||
|
|
||||||
def test_home_deletes_withdrawn_domain_request(self):
|
|
||||||
"""Tests if the user can delete a DomainRequest in the 'withdrawn' status"""
|
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
domain_request = DomainRequest.objects.create(
|
|
||||||
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.WITHDRAWN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure that igorville.gov exists on the page
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
|
|
||||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
|
||||||
self.assertContains(home_page, "Delete")
|
|
||||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
|
||||||
|
|
||||||
self.assertNotContains(response, "igorville.gov")
|
|
||||||
|
|
||||||
# clean up
|
|
||||||
domain_request.delete()
|
|
||||||
|
|
||||||
def test_home_deletes_started_domain_request(self):
|
|
||||||
"""Tests if the user can delete a DomainRequest in the 'started' status"""
|
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
domain_request = DomainRequest.objects.create(
|
|
||||||
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.STARTED
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure that igorville.gov exists on the page
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
|
|
||||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
|
||||||
self.assertContains(home_page, "Delete")
|
|
||||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
|
||||||
|
|
||||||
self.assertNotContains(response, "igorville.gov")
|
|
||||||
|
|
||||||
# clean up
|
|
||||||
domain_request.delete()
|
|
||||||
|
|
||||||
def test_home_doesnt_delete_other_domain_requests(self):
|
|
||||||
"""Tests to ensure the user can't delete domain requests not in the status of STARTED or WITHDRAWN"""
|
|
||||||
|
|
||||||
# Given that we are including a subset of items that can be deleted while excluding the rest,
|
|
||||||
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
|
|
||||||
with less_console_noise():
|
|
||||||
draft_domain = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
for status in DomainRequest.DomainRequestStatus:
|
|
||||||
if status not in [
|
|
||||||
DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
]:
|
|
||||||
with self.subTest(status=status):
|
|
||||||
domain_request = DomainRequest.objects.create(
|
|
||||||
creator=self.user, requested_domain=draft_domain, status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for a 403 error - the end user should not be allowed to do this
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
desired_domain_request = DomainRequest.objects.filter(requested_domain=draft_domain)
|
|
||||||
|
|
||||||
# Make sure the DomainRequest wasn't deleted
|
|
||||||
self.assertEqual(desired_domain_request.count(), 1)
|
|
||||||
|
|
||||||
# clean up
|
|
||||||
domain_request.delete()
|
|
||||||
|
|
||||||
def test_home_deletes_domain_request_and_orphans(self):
|
|
||||||
"""Tests if delete for DomainRequest deletes orphaned Contact objects"""
|
|
||||||
|
|
||||||
# Create the site and contacts to delete (orphaned)
|
|
||||||
contact = Contact.objects.create(
|
|
||||||
first_name="Henry",
|
|
||||||
last_name="Mcfakerson",
|
|
||||||
)
|
|
||||||
contact_shared = Contact.objects.create(
|
|
||||||
first_name="Relative",
|
|
||||||
last_name="Aether",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create two non-orphaned contacts
|
|
||||||
contact_2 = Contact.objects.create(
|
|
||||||
first_name="Saturn",
|
|
||||||
last_name="Mars",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attach a user object to a contact (should not be deleted)
|
|
||||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
domain_request = DomainRequest.objects.create(
|
|
||||||
creator=self.user,
|
|
||||||
requested_domain=site,
|
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
authorizing_official=contact,
|
|
||||||
submitter=contact_user,
|
|
||||||
)
|
|
||||||
domain_request.other_contacts.set([contact_2])
|
|
||||||
|
|
||||||
# Create a second domain request to attach contacts to
|
|
||||||
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
|
||||||
domain_request_2 = DomainRequest.objects.create(
|
|
||||||
creator=self.user,
|
|
||||||
requested_domain=site_2,
|
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
authorizing_official=contact_2,
|
|
||||||
submitter=contact_shared,
|
|
||||||
)
|
|
||||||
domain_request_2.other_contacts.set([contact_shared])
|
|
||||||
|
|
||||||
# Ensure that igorville.gov exists on the page
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
|
||||||
|
|
||||||
# igorville is now deleted
|
|
||||||
self.assertNotContains(response, "igorville.gov")
|
|
||||||
|
|
||||||
# Check if the orphaned contact was deleted
|
|
||||||
orphan = Contact.objects.filter(id=contact.id)
|
|
||||||
self.assertFalse(orphan.exists())
|
|
||||||
|
|
||||||
# All non-orphan contacts should still exist and are unaltered
|
|
||||||
try:
|
|
||||||
current_user = Contact.objects.filter(id=contact_user.id).get()
|
|
||||||
except Contact.DoesNotExist:
|
|
||||||
self.fail("contact_user (a non-orphaned contact) was deleted")
|
|
||||||
|
|
||||||
self.assertEqual(current_user, contact_user)
|
|
||||||
try:
|
|
||||||
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
|
||||||
except Contact.DoesNotExist:
|
|
||||||
self.fail("contact_2 (a non-orphaned contact) was deleted")
|
|
||||||
|
|
||||||
self.assertEqual(edge_case, contact_2)
|
|
||||||
|
|
||||||
def test_home_deletes_domain_request_and_shared_orphans(self):
|
|
||||||
"""Test the edge case for an object that will become orphaned after a delete
|
|
||||||
(but is not an orphan at the time of deletion)"""
|
|
||||||
|
|
||||||
# Create the site and contacts to delete (orphaned)
|
|
||||||
contact = Contact.objects.create(
|
|
||||||
first_name="Henry",
|
|
||||||
last_name="Mcfakerson",
|
|
||||||
)
|
|
||||||
contact_shared = Contact.objects.create(
|
|
||||||
first_name="Relative",
|
|
||||||
last_name="Aether",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create two non-orphaned contacts
|
|
||||||
contact_2 = Contact.objects.create(
|
|
||||||
first_name="Saturn",
|
|
||||||
last_name="Mars",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attach a user object to a contact (should not be deleted)
|
|
||||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
domain_request = DomainRequest.objects.create(
|
|
||||||
creator=self.user,
|
|
||||||
requested_domain=site,
|
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
authorizing_official=contact,
|
|
||||||
submitter=contact_user,
|
|
||||||
)
|
|
||||||
domain_request.other_contacts.set([contact_2])
|
|
||||||
|
|
||||||
# Create a second domain request to attach contacts to
|
|
||||||
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
|
||||||
domain_request_2 = DomainRequest.objects.create(
|
|
||||||
creator=self.user,
|
|
||||||
requested_domain=site_2,
|
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
authorizing_official=contact_2,
|
|
||||||
submitter=contact_shared,
|
|
||||||
)
|
|
||||||
domain_request_2.other_contacts.set([contact_shared])
|
|
||||||
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertContains(home_page, "teaville.gov")
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request_2.pk}), follow=True)
|
|
||||||
|
|
||||||
self.assertNotContains(response, "teaville.gov")
|
|
||||||
|
|
||||||
# Check if the orphaned contact was deleted
|
|
||||||
orphan = Contact.objects.filter(id=contact_shared.id)
|
|
||||||
self.assertFalse(orphan.exists())
|
|
||||||
|
|
||||||
def test_domain_request_form_view(self):
|
|
||||||
response = self.client.get("/request/", follow=True)
|
|
||||||
self.assertContains(
|
|
||||||
response,
|
|
||||||
"You’re about to start your .gov domain request.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_domain_request_form_with_ineligible_user(self):
|
|
||||||
"""Domain request form not accessible for an ineligible user.
|
|
||||||
This test should be solid enough since all domain request wizard
|
|
||||||
views share the same permissions class"""
|
|
||||||
self.user.status = User.RESTRICTED
|
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
with less_console_noise():
|
|
||||||
response = self.client.get("/request/", follow=True)
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ class Step(StrEnum):
|
||||||
PURPOSE = "purpose"
|
PURPOSE = "purpose"
|
||||||
YOUR_CONTACT = "your_contact"
|
YOUR_CONTACT = "your_contact"
|
||||||
OTHER_CONTACTS = "other_contacts"
|
OTHER_CONTACTS = "other_contacts"
|
||||||
ANYTHING_ELSE = "anything_else"
|
ADDITIONAL_DETAILS = "additional_details"
|
||||||
REQUIREMENTS = "requirements"
|
REQUIREMENTS = "requirements"
|
||||||
REVIEW = "review"
|
REVIEW = "review"
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
Step.PURPOSE: _("Purpose of your domain"),
|
Step.PURPOSE: _("Purpose of your domain"),
|
||||||
Step.YOUR_CONTACT: _("Your contact information"),
|
Step.YOUR_CONTACT: _("Your contact information"),
|
||||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||||
Step.ANYTHING_ELSE: _("Anything else?"),
|
Step.ADDITIONAL_DETAILS: _("Additional details"),
|
||||||
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||||
Step.REVIEW: _("Review and submit your domain request"),
|
Step.REVIEW: _("Review and submit your domain request"),
|
||||||
}
|
}
|
||||||
|
@ -365,8 +365,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
self.domain_request.other_contacts.exists()
|
self.domain_request.other_contacts.exists()
|
||||||
or self.domain_request.no_other_contacts_rationale is not None
|
or self.domain_request.no_other_contacts_rationale is not None
|
||||||
),
|
),
|
||||||
"anything_else": (
|
"additional_details": (
|
||||||
self.domain_request.anything_else is not None or self.domain_request.is_policy_acknowledged is not None
|
(self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email)
|
||||||
|
or self.domain_request.is_policy_acknowledged is not None
|
||||||
),
|
),
|
||||||
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
||||||
"review": self.domain_request.is_policy_acknowledged is not None,
|
"review": self.domain_request.is_policy_acknowledged is not None,
|
||||||
|
@ -581,9 +582,64 @@ class OtherContacts(DomainRequestWizard):
|
||||||
return all_forms_valid
|
return all_forms_valid
|
||||||
|
|
||||||
|
|
||||||
class AnythingElse(DomainRequestWizard):
|
class AdditionalDetails(DomainRequestWizard):
|
||||||
template_name = "domain_request_anything_else.html"
|
|
||||||
forms = [forms.AnythingElseForm]
|
template_name = "domain_request_additional_details.html"
|
||||||
|
|
||||||
|
forms = [
|
||||||
|
forms.CisaRepresentativeYesNoForm,
|
||||||
|
forms.CisaRepresentativeForm,
|
||||||
|
forms.AdditionalDetailsYesNoForm,
|
||||||
|
forms.AdditionalDetailsForm,
|
||||||
|
]
|
||||||
|
|
||||||
|
def is_valid(self, forms: list) -> bool:
|
||||||
|
|
||||||
|
# Validate Cisa Representative
|
||||||
|
"""Overrides default behavior defined in DomainRequestWizard.
|
||||||
|
Depending on value in yes_no forms, marks corresponding data
|
||||||
|
for deletion. Then validates all forms.
|
||||||
|
"""
|
||||||
|
cisa_representative_email_yes_no_form = forms[0]
|
||||||
|
cisa_representative_email_form = forms[1]
|
||||||
|
anything_else_yes_no_form = forms[2]
|
||||||
|
anything_else_form = forms[3]
|
||||||
|
|
||||||
|
# ------- Validate cisa representative -------
|
||||||
|
cisa_rep_portion_is_valid = True
|
||||||
|
# test first for yes_no_form validity
|
||||||
|
if cisa_representative_email_yes_no_form.is_valid():
|
||||||
|
# test for existing data
|
||||||
|
if not cisa_representative_email_yes_no_form.cleaned_data.get("has_cisa_representative"):
|
||||||
|
# mark the cisa_representative_email_form for deletion
|
||||||
|
cisa_representative_email_form.mark_form_for_deletion()
|
||||||
|
else:
|
||||||
|
cisa_rep_portion_is_valid = cisa_representative_email_form.is_valid()
|
||||||
|
else:
|
||||||
|
# if yes no form is invalid, no choice has been made
|
||||||
|
# mark the cisa_representative_email_form for deletion
|
||||||
|
cisa_representative_email_form.mark_form_for_deletion()
|
||||||
|
cisa_rep_portion_is_valid = False
|
||||||
|
|
||||||
|
# ------- Validate anything else -------
|
||||||
|
anything_else_portion_is_valid = True
|
||||||
|
# test first for yes_no_form validity
|
||||||
|
if anything_else_yes_no_form.is_valid():
|
||||||
|
# test for existing data
|
||||||
|
if not anything_else_yes_no_form.cleaned_data.get("has_anything_else_text"):
|
||||||
|
# mark the anything_else_form for deletion
|
||||||
|
anything_else_form.mark_form_for_deletion()
|
||||||
|
else:
|
||||||
|
anything_else_portion_is_valid = anything_else_form.is_valid()
|
||||||
|
else:
|
||||||
|
# if yes no form is invalid, no choice has been made
|
||||||
|
# mark the anything_else_form for deletion
|
||||||
|
anything_else_form.mark_form_for_deletion()
|
||||||
|
anything_else_portion_is_valid = False
|
||||||
|
|
||||||
|
# ------- Return combined validation result -------
|
||||||
|
all_forms_valid = cisa_rep_portion_is_valid and anything_else_portion_is_valid
|
||||||
|
return all_forms_valid
|
||||||
|
|
||||||
|
|
||||||
class Requirements(DomainRequestWizard):
|
class Requirements(DomainRequestWizard):
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
-i https://pypi.python.org/simple
|
-i https://pypi.python.org/simple
|
||||||
annotated-types==0.6.0; python_version >= '3.8'
|
annotated-types==0.6.0; python_version >= '3.8'
|
||||||
asgiref==3.8.1; python_version >= '3.8'
|
asgiref==3.8.1; python_version >= '3.8'
|
||||||
boto3==1.34.88; python_version >= '3.8'
|
boto3==1.34.90; python_version >= '3.8'
|
||||||
botocore==1.34.88; python_version >= '3.8'
|
botocore==1.34.90; python_version >= '3.8'
|
||||||
cachetools==5.3.3; python_version >= '3.7'
|
cachetools==5.3.3; python_version >= '3.7'
|
||||||
certifi==2024.2.2; python_version >= '3.6'
|
certifi==2024.2.2; python_version >= '3.6'
|
||||||
cfenv==0.5.3
|
cfenv==0.5.3
|
||||||
|
@ -16,7 +16,7 @@ dj-email-url==1.0.6
|
||||||
django==4.2.10; python_version >= '3.8'
|
django==4.2.10; python_version >= '3.8'
|
||||||
django-admin-multiple-choice-list-filter==0.1.1
|
django-admin-multiple-choice-list-filter==0.1.1
|
||||||
django-allow-cidr==0.7.1
|
django-allow-cidr==0.7.1
|
||||||
django-auditlog==2.3.0; python_version >= '3.7'
|
django-auditlog==3.0.0; python_version >= '3.8'
|
||||||
django-cache-url==3.4.5
|
django-cache-url==3.4.5
|
||||||
django-cors-headers==4.3.1; python_version >= '3.8'
|
django-cors-headers==4.3.1; python_version >= '3.8'
|
||||||
django-csp==3.8
|
django-csp==3.8
|
||||||
|
@ -50,8 +50,8 @@ phonenumberslite==8.13.35
|
||||||
psycopg2-binary==2.9.9; python_version >= '3.7'
|
psycopg2-binary==2.9.9; python_version >= '3.7'
|
||||||
pycparser==2.22; python_version >= '3.8'
|
pycparser==2.22; python_version >= '3.8'
|
||||||
pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
pydantic==2.7.0; python_version >= '3.8'
|
pydantic==2.7.1; python_version >= '3.8'
|
||||||
pydantic-core==2.18.1; python_version >= '3.8'
|
pydantic-core==2.18.2; python_version >= '3.8'
|
||||||
pydantic-settings==2.2.1; python_version >= '3.8'
|
pydantic-settings==2.2.1; python_version >= '3.8'
|
||||||
pyjwkest==1.4.2
|
pyjwkest==1.4.2
|
||||||
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue