diff --git a/.github/workflows/deploy-development.yaml b/.github/workflows/deploy-development.yaml
index 686635c20..e62607d95 100644
--- a/.github/workflows/deploy-development.yaml
+++ b/.github/workflows/deploy-development.yaml
@@ -22,9 +22,16 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
- docker compose run node npm install &&
- docker compose run node npx gulp copyAssets &&
- docker compose run node npx gulp compile
+ docker compose run node bash -c "\
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
+ 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
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input
@@ -45,4 +52,4 @@ jobs:
cf_password: ${{ secrets.CF_DEVELOPMENT_PASSWORD }}
cf_org: cisa-dotgov
cf_space: development
- cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate"
+ cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate"
\ No newline at end of file
diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml
index f7f4a0d65..d9d7cbe14 100644
--- a/.github/workflows/deploy-sandbox.yaml
+++ b/.github/workflows/deploy-sandbox.yaml
@@ -42,9 +42,16 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
- docker compose run node npm install &&
- docker compose run node npx gulp copyAssets &&
- docker compose run node npx gulp compile
+ docker compose run node bash -c "\
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
+ 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
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input
@@ -75,4 +82,4 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.'
- })
+ })
\ No newline at end of file
diff --git a/.github/workflows/deploy-stable.yaml b/.github/workflows/deploy-stable.yaml
index 0ded4a3a6..9d0573e01 100644
--- a/.github/workflows/deploy-stable.yaml
+++ b/.github/workflows/deploy-stable.yaml
@@ -23,9 +23,16 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
- docker compose run node npm install &&
- docker compose run node npx gulp copyAssets &&
- docker compose run node npx gulp compile
+ docker compose run node bash -c "\
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
+ 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
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input
diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml
index 1df08f412..9584985f0 100644
--- a/.github/workflows/deploy-staging.yaml
+++ b/.github/workflows/deploy-staging.yaml
@@ -23,9 +23,16 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
- docker compose run node npm install &&
- docker compose run node npx gulp copyAssets &&
- docker compose run node npx gulp compile
+ docker compose run node bash -c "\
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
+ 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
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input
@@ -44,4 +51,4 @@ jobs:
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
cf_org: cisa-dotgov
cf_space: staging
- cf_command: "run-task getgov-staging --command 'python manage.py migrate' --name migrate"
+ cf_command: "run-task getgov-staging --command 'python manage.py migrate' --name migrate"
\ No newline at end of file
diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md
index 0846208de..e4543a28c 100644
--- a/docs/operations/data_migration.md
+++ b/docs/operations/data_migration.md
@@ -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).
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.
-#### Step 2: SSH into your environment
+#### Step 4: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
-#### Step 3: Create a shell instance
+#### Step 5: Create a shell instance
```/tmp/lifecycle/shell```
-#### Step 4: Running the script
+#### Step 6: Running the script
```./manage.py populate_organization_type {domain_election_board_filename}```
- 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 |
|:-:|:------------------------------------|:-------------------------------------------------------------------|
| 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```
diff --git a/src/Pipfile b/src/Pipfile
index c1f03f532..33abf0158 100644
--- a/src/Pipfile
+++ b/src/Pipfile
@@ -9,7 +9,7 @@ cfenv = "*"
django-cors-headers = "*"
pycryptodomex = "*"
django-allow-cidr = "*"
-django-auditlog = "2.3.0"
+django-auditlog = "*"
django-csp = "*"
environs = {extras=["django"]}
Faker = "*"
diff --git a/src/Pipfile.lock b/src/Pipfile.lock
index e485a5202..f21b833ce 100644
--- a/src/Pipfile.lock
+++ b/src/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "99cf9a4f3912639c02105889046a9eede7a29822fab6f9a04ca25f95e29513c0"
+ "sha256": "16a0db98015509322cf1d27f06fced5b7635057c4eb98921a9419d63d51925ab"
},
"pipfile-spec": 6,
"requires": {},
@@ -32,20 +32,20 @@
},
"boto3": {
"hashes": [
- "sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87",
- "sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3"
+ "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
+ "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.34.88"
+ "version": "==1.34.90"
},
"botocore": {
"hashes": [
- "sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e",
- "sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b"
+ "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
+ "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
],
"markers": "python_version >= '3.8'",
- "version": "==1.34.88"
+ "version": "==1.34.90"
},
"cachetools": {
"hashes": [
@@ -321,12 +321,12 @@
},
"django-auditlog": {
"hashes": [
- "sha256:7bc2c87e4aff62dec9785d1b2359a2b27148f8c286f8a52b9114fc7876c5a9f7",
- "sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532"
+ "sha256:92db1cf4a51ceca5c26b3ff46997d9e3305a02da1bd435e2efb5b8b6d300ce1f",
+ "sha256:9de49f80a4911135d136017123cd73461f869b4947eec14d5e76db4b88182f3f"
],
"index": "pypi",
- "markers": "python_version >= '3.7'",
- "version": "==2.3.0"
+ "markers": "python_version >= '3.8'",
+ "version": "==3.0.0"
},
"django-cache-url": {
"hashes": [
@@ -1003,96 +1003,96 @@
},
"pydantic": {
"hashes": [
- "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352",
- "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"
+ "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5",
+ "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"
],
"markers": "python_version >= '3.8'",
- "version": "==2.7.0"
+ "version": "==2.7.1"
},
"pydantic-core": {
"hashes": [
- "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6",
- "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb",
- "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0",
- "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6",
- "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47",
- "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a",
- "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a",
- "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac",
- "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88",
- "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db",
- "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d",
- "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d",
- "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9",
- "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e",
- "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b",
- "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d",
- "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649",
- "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c",
- "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1",
- "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09",
- "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0",
- "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90",
- "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d",
- "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294",
- "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144",
- "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b",
- "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1",
- "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b",
- "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2",
- "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad",
- "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622",
- "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17",
- "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06",
- "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc",
- "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50",
- "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d",
- "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59",
- "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539",
- "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a",
- "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b",
- "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5",
- "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9",
- "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278",
- "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6",
- "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44",
- "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0",
- "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb",
- "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80",
- "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5",
- "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570",
- "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b",
- "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de",
- "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6",
- "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8",
- "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203",
- "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7",
- "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048",
- "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae",
- "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89",
- "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f",
- "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926",
- "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2",
- "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76",
- "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d",
- "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411",
- "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9",
- "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2",
- "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586",
- "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35",
- "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c",
- "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143",
- "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6",
- "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60",
- "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b",
- "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226",
- "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519",
- "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31",
- "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7",
- "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"
+ "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b",
+ "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a",
+ "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90",
+ "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d",
+ "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e",
+ "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d",
+ "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027",
+ "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804",
+ "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347",
+ "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400",
+ "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3",
+ "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399",
+ "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349",
+ "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd",
+ "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c",
+ "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e",
+ "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413",
+ "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3",
+ "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e",
+ "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3",
+ "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91",
+ "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce",
+ "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c",
+ "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb",
+ "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664",
+ "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6",
+ "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd",
+ "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3",
+ "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af",
+ "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043",
+ "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350",
+ "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7",
+ "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0",
+ "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563",
+ "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761",
+ "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72",
+ "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3",
+ "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb",
+ "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788",
+ "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b",
+ "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c",
+ "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038",
+ "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250",
+ "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec",
+ "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c",
+ "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74",
+ "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81",
+ "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439",
+ "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75",
+ "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0",
+ "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8",
+ "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150",
+ "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438",
+ "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae",
+ "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857",
+ "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038",
+ "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374",
+ "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f",
+ "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241",
+ "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592",
+ "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4",
+ "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d",
+ "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b",
+ "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b",
+ "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182",
+ "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e",
+ "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641",
+ "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70",
+ "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9",
+ "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a",
+ "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543",
+ "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b",
+ "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f",
+ "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38",
+ "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845",
+ "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2",
+ "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0",
+ "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4",
+ "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"
],
"markers": "python_version >= '3.8'",
- "version": "==2.18.1"
+ "version": "==2.18.2"
},
"pydantic-settings": {
"hashes": [
@@ -1411,12 +1411,12 @@
},
"boto3": {
"hashes": [
- "sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87",
- "sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3"
+ "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
+ "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.34.88"
+ "version": "==1.34.90"
},
"boto3-mocking": {
"hashes": [
@@ -1429,28 +1429,28 @@
},
"boto3-stubs": {
"hashes": [
- "sha256:23ca9e0cd0d3e7702d6631a1e94a4208a26b39fa6b12c734427e68a7fa649477",
- "sha256:8f472d1bf09743c3d33304ecc8830d70ebe3ca19ac9604ae8da9af55421b0fce"
+ "sha256:7361f162523168ddcfb3e0cc70e5208e78f95b9f1f2553032036a2b67ab33355",
+ "sha256:c82f3db8558e28f766361ba1eea7c77dff735f72fef2a0b9dffaa9c0d9ae76a3"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.34.88"
+ "version": "==1.34.90"
},
"botocore": {
"hashes": [
- "sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e",
- "sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b"
+ "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
+ "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
],
"markers": "python_version >= '3.8'",
- "version": "==1.34.88"
+ "version": "==1.34.90"
},
"botocore-stubs": {
"hashes": [
- "sha256:656e966ea152a4f2828892aa7a9673bc91799998f5a8efd8e8fe390f61c2f4f1",
- "sha256:f55b03ae2e1706bd56299fd2975bb048f96aa49012a866e931a040a74f85c3cc"
+ "sha256:b2d7416b524bce7325aa5fe09bb5e0b6bc9531d4136f4407fa39b6bc58507f34",
+ "sha256:d9b66542cbb8fbe28eef3c22caf941ae22d36cc1ef55b93fc0b52239457cab57"
],
"markers": "python_version >= '3.8' and python_version < '4.0'",
- "version": "==1.34.88"
+ "version": "==1.34.89"
},
"click": {
"hashes": [
@@ -1627,11 +1627,11 @@
},
"platformdirs": {
"hashes": [
- "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
- "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
+ "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf",
+ "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"
],
"markers": "python_version >= '3.8'",
- "version": "==4.2.0"
+ "version": "==4.2.1"
},
"pycodestyle": {
"hashes": [
diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py
index f10afcbaf..bdd61b346 100644
--- a/src/djangooidc/tests/test_views.py
+++ b/src/djangooidc/tests/test_views.py
@@ -4,8 +4,10 @@ from django.http import HttpResponse
from django.test import Client, TestCase, RequestFactory
from django.urls import reverse
+from api.tests.common import less_console_noise_decorator
from djangooidc.exceptions import StateMismatch, InternalError
from ..views import login_callback
+from registrar.models import User, Contact, VerifiedByStaff, DomainInvitation, TransitionDomain, Domain
from .common import less_console_noise
@@ -16,6 +18,14 @@ class ViewsTest(TestCase):
self.client = Client()
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):
return HttpResponse("Hi")
@@ -229,6 +239,140 @@ class ViewsTest(TestCase):
self.assertEqual(response.status_code, 302)
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):
"""Walk through login_callback when _requires_step_up_auth returns False
and assert that we have a redirect to /"""
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index ab81ccff1..815df4ecf 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -99,8 +99,22 @@ def login_callback(request):
return CLIENT.create_authn_request(request.session)
user = authenticate(request=request, **userinfo)
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)
logger.info("Successfully logged in user %s" % user)
+
# Clear the flag if the exception is not caught
request.session.pop("redirect_attempted", None)
return redirect(request.session.get("next", "/"))
diff --git a/src/docker-compose.yml b/src/docker-compose.yml
index c69c21192..1a9064ac8 100644
--- a/src/docker-compose.yml
+++ b/src/docker-compose.yml
@@ -108,7 +108,7 @@ services:
- pa11y
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
volumes:
- .:/zap/wrk/
diff --git a/src/node.Dockerfile b/src/node.Dockerfile
index b478a8a26..cf0b6acc6 100644
--- a/src/node.Dockerfile
+++ b/src/node.Dockerfile
@@ -1,5 +1,5 @@
FROM docker.io/cimg/node:current-browsers
-
+FROM node:21.7.3
WORKDIR /app
# Install app dependencies
@@ -7,4 +7,6 @@ WORKDIR /app
# where available (npm@5+)
COPY --chown=circleci:circleci package*.json ./
-RUN npm install
+
+RUN npm install -g npm@10.5.0
+RUN npm install
\ No newline at end of file
diff --git a/src/package-lock.json b/src/package-lock.json
index dc1464ee8..9df99a739 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -15,6 +15,10 @@
},
"devDependencies": {
"@uswds/compile": "^1.0.0-beta.3"
+ },
+ "engines": {
+ "node": "21.7.3",
+ "npm": "10.5.0"
}
},
"node_modules/@gulp-sourcemaps/identity-map": {
diff --git a/src/package.json b/src/package.json
index 274e0e282..3afce297f 100644
--- a/src/package.json
+++ b/src/package.json
@@ -3,6 +3,11 @@
"version": "1.0.0",
"description": "========================",
"main": "index.js",
+ "engines": {
+ "node": "21.7.3",
+ "npm": "10.5.0"
+ },
+ "engineStrict": true,
"scripts": {
"pa11y-ci": "pa11y-ci",
"test": "echo \"Error: no test specified\" && exit 1"
@@ -17,4 +22,4 @@
"devDependencies": {
"@uswds/compile": "^1.0.0-beta.3"
}
-}
+}
\ No newline at end of file
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 7231c634e..744babb95 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -119,6 +119,34 @@ class MyUserAdminForm(UserChangeForm):
"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 this form.'
+ )
+
class DomainInformationAdminForm(forms.ModelForm):
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
@@ -582,7 +610,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
fieldsets = (
(
None,
- {"fields": ("username", "password", "status")},
+ {"fields": ("username", "password", "status", "verification_type")},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
@@ -600,13 +628,20 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
("Important dates", {"fields": ("last_login", "date_joined")}),
)
+ readonly_fields = ("verification_type",)
+
# Hide Username (uuid), Groups and Permissions
# Q: Now that we're using Groups and Permissions,
# do we expose those to analysts to view?
analyst_fieldsets = (
(
None,
- {"fields": ("password", "status")},
+ {
+ "fields": (
+ "status",
+ "verification_type",
+ )
+ },
),
("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
# set this at the permissions level. The exception is 'status'
analyst_readonly_fields = [
- "password",
"Personal Info",
"first_name",
"last_name",
@@ -727,11 +761,14 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
return []
def get_readonly_fields(self, request, obj=None):
+ readonly_fields = list(self.readonly_fields)
+
if request.user.has_perm("registrar.full_access_permission"):
- return () # No read-only fields for all access users
- # Return restrictive Read-only fields for analysts and
- # users who might not belong to groups
- return self.analyst_readonly_fields
+ return readonly_fields
+ else:
+ # Return restrictive Read-only fields for analysts and
+ # users who might not belong to groups
+ return self.analyst_readonly_fields
class HostIPInline(admin.StackedInline):
@@ -1063,9 +1100,10 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
},
),
(
- "More details",
+ "Show details",
{
- "classes": ["collapse"],
+ "classes": ["collapse--dotgov"],
+ "description": "Extends type of organization",
"fields": [
"federal_type",
# "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": [
"address_line1",
"address_line2",
@@ -1299,7 +1338,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
},
),
(".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"]}),
(
"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": [
"federal_type",
# "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": [
"address_line1",
"address_line2",
@@ -1372,6 +1423,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
+ "cisa_representative_email",
]
autocomplete_fields = [
"approved_domain",
@@ -1827,21 +1879,27 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if domain is not None and hasattr(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
- years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
- try:
- curr_exp_date = domain.registry_expiration_date
- except KeyError:
- # No expiration date was found. Return none.
- extra_context["extended_expiration_date"] = None
- return super().changeform_view(request, object_id, form_url, extra_context)
- new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
- extra_context["extended_expiration_date"] = new_date
- else:
- extra_context["extended_expiration_date"] = None
+ 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)
+ try:
+ curr_exp_date = domain.registry_expiration_date
+ except KeyError:
+ # No expiration date was found. Return none.
+ extra_context["extended_expiration_date"] = None
+ else:
+ new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
+ extra_context["extended_expiration_date"] = new_date
+
def response_change(self, request, obj):
# Create dictionary of action functions
ACTION_FUNCTIONS = {
diff --git a/src/registrar/assets/js/dja-collapse.js b/src/registrar/assets/js/dja-collapse.js
new file mode 100644
index 000000000..c33954192
--- /dev/null
+++ b/src/registrar/assets/js/dja-collapse.js
@@ -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);
+ });
+ });
+}
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index 2909a48be..f38afd252 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -233,10 +233,8 @@ function openInNewTab(el, removeAttribute = false){
// 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
// "to" select list
- checkToListThenInitWidget('id_other_contacts_to', 0);
- checkToListThenInitWidget('id_domain_info-0-other_contacts_to', 0);
- checkToListThenInitWidget('id_current_websites_to', 0);
- checkToListThenInitWidget('id_alternative_domains_to', 0);
+ checkToListThenInitWidget('id_groups_to', 0);
+ checkToListThenInitWidget('id_user_permissions_to', 0);
})();
// 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);
attempts++;
- if (attempts < 6) {
- if ((toList !== null)) {
+ if (attempts < 12) {
+ if (toList) {
// toList found, handle it
- // Add an event listener on the element
- // Add disabled buttons on the element's great-grandparent
- initializeWidgetOnToList(toList, toListId);
+ // Then get fromList and handle it
+ initializeWidgetOnList(toList, ".selector-chosen");
+ let fromList = toList.closest('.selector').querySelector(".selector-available select");
+ initializeWidgetOnList(fromList, ".selector-available");
} else {
// 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:
-// add related buttons to the widget for edit, delete and view
-// add event listeners on the from list, the to list, and selector buttons which either enable or disable the related buttons
-function initializeWidgetOnToList(toList, toListId) {
- // create the change button
- let changeLink = createAndCustomizeLink(
- toList,
- 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
- );
+// Replace h2 with more semantic h3
+function initializeWidgetOnList(list, parentId) {
+ if (list) {
+ // Get h2 and its container
+ const parentElement = list.closest(parentId);
+ const h2Element = parentElement.querySelector('h2');
- let hasDeletePermission = hasDeletePermissionOnPage();
+ // One last check
+ if (parentElement && h2Element) {
+ // Create a new
element
+ const h3Element = document.createElement('h3');
- let deleteLink = null;
- if (hasDeletePermission) {
- // create the delete button if user has permission to delete
- deleteLink = createAndCustomizeLink(
- toList,
- toListId,
- 'related-widget-wrapper-link delete-related',
- 'Delete',
- '/public/admin/img/icon-deletelink.svg',
- {
- 'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1',
- 'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
- 'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
- },
- true,
- false
- );
- }
+ // Copy the text content from the
element to the
element
+ h3Element.textContent = h2Element.textContent;
- // create the view button
- let viewLink = createAndCustomizeLink(
- 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
- );
+ // Find the nested element inside the
+ const nestedSpan = h2Element.querySelector('span[class][title]');
- // identify the fromList element in the DOM
- let fromList = toList.closest('.selector').querySelector(".selector-available select");
+ // If the nested element exists
+ if (nestedSpan) {
+ // Create a new element
+ const newSpan = document.createElement('span');
- 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"));
+ // Copy the class and title attributes from the nested element
+ newSpan.className = nestedSpan.className;
+ newSpan.title = nestedSpan.title;
- 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;
+ // Append the new element to the
element
+ h3Element.appendChild(newSpan);
}
- relatedWidgetWrapper.insertBefore(link, previousSibling.nextSibling);
+
+ // Replace the
element with the new
element
+ parentElement.replaceChild(h3Element, h2Element);
}
}
-
- // 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
- * status select amd to show/hide the rejection reason
+ * status select and to show/hide the rejection reason
*/
(function (){
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index b4c41ecf1..e7260ee21 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -193,6 +193,65 @@ function clearValidators(el) {
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.
@@ -712,58 +771,41 @@ 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
*
*/
(function otherContactsFormListener() {
- // Get the radio buttons
- let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]');
+ HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees')
+})();
- 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':
- toggleTwoDomElements('other-employees', 'no-other-employees', 1);
- break;
-
- case 'False':
- toggleTwoDomElements('other-employees', 'no-other-employees', 2);
- break;
-
- default:
- toggleTwoDomElements('other-employees', 'no-other-employees', 0);
+/**
+ * An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
+ *
+ */
+(function nameserversFormListener() {
+ let isNameserversForm = document.querySelector(".nameservers-form");
+ if (isNameserversForm) {
+ let forms = document.querySelectorAll(".repeatable-form");
+ if (forms.length < 3) {
+ // Hide the delete buttons on the 2 nameservers
+ forms.forEach((form) => {
+ Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
+ deleteButton.setAttribute("disabled", "true");
+ });
+ });
}
}
-
- 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)
+})();
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index f5717d067..b4b590acb 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -112,12 +112,20 @@ html[data-theme="light"] {
.change-list .usa-table--borderless thead th,
.change-list .usa-table thead td,
.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.change-list,
body.change-form,
.analytics {
color: var(--body-fg);
}
+ .usa-table td {
+ background-color: transparent;
+ }
}
// 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 thead td,
.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.change-list,
- body.change-form {
+ body.change-form,
+ .analytics {
color: var(--body-fg);
}
+ .usa-table td {
+ background-color: transparent;
+ }
}
#branding h1 a:link, #branding h1 a:visited {
@@ -525,17 +542,30 @@ address.dja-address-contact-list {
}
// Collapse button styles for fieldsets
-.module.collapse {
+.module.collapse--dotgov {
margin-top: -35px;
padding-top: 0;
border: none;
- h2 {
+ button {
background: none;
- color: var(--body-fg)!important;
text-transform: none;
- }
- a {
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 {
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);
+}
diff --git a/src/registrar/assets/sass/_theme/_links.scss b/src/registrar/assets/sass/_theme/_links.scss
new file mode 100644
index 000000000..6d2e75a68
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_links.scss
@@ -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);
+ }
+}
diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss
index 0d58b5878..26d90d291 100644
--- a/src/registrar/assets/sass/_theme/_tables.scss
+++ b/src/registrar/assets/sass/_theme/_tables.scss
@@ -56,22 +56,6 @@
.dotgov-table {
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 {
right: auto;
}
@@ -108,12 +92,51 @@
padding: units(2) units(2) units(2) 0;
}
- th:first-of-type {
- padding-left: 0;
- }
-
thead tr:first-child th:first-child {
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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss
index 942501110..64b113a29 100644
--- a/src/registrar/assets/sass/_theme/styles.scss
+++ b/src/registrar/assets/sass/_theme/styles.scss
@@ -10,6 +10,7 @@
--- Custom Styles ---------------------------------*/
@forward "base";
@forward "typography";
+@forward "links";
@forward "lists";
@forward "buttons";
@forward "forms";
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index 62e676839..062e2492e 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -782,3 +782,11 @@ if DEBUG:
# due to Docker, bypass Debug Toolbar's check on INTERNAL_IPS
"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
diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py
index 3918fa087..720034150 100644
--- a/src/registrar/config/urls.py
+++ b/src/registrar/config/urls.py
@@ -46,7 +46,7 @@ for step, view in [
(Step.PURPOSE, views.Purpose),
(Step.YOUR_CONTACT, views.YourContact),
(Step.OTHER_CONTACTS, views.OtherContacts),
- (Step.ANYTHING_ELSE, views.AnythingElse),
+ (Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
(Step.REQUIREMENTS, views.Requirements),
(Step.REVIEW, views.Review),
]:
diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py
index 99fe4910e..7f991fa0e 100644
--- a/src/registrar/fixtures_users.py
+++ b/src/registrar/fixtures_users.py
@@ -7,6 +7,7 @@ from registrar.models import (
UserGroup,
)
+
fake = Faker()
logger = logging.getLogger(__name__)
@@ -93,6 +94,12 @@ class UserFixture:
"last_name": "Chin",
"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",
"first_name": "Riley",
@@ -169,6 +176,12 @@ class UserFixture:
"last_name": "Chin-Analyst",
"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",
"first_name": "Alex-Analyst",
@@ -195,6 +208,10 @@ class UserFixture:
user.email = user_data["email"]
user.is_staff = 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)
user.groups.add(group)
user.save()
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index c3ac3b4c2..8d74f6f35 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -1,15 +1,18 @@
from __future__ import annotations # allows forward references in annotations
-from itertools import zip_longest
import logging
-from typing import Callable
from api.views import DOMAIN_API_MESSAGES
from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator
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.templatetags.url_helpers import public_site_url
from registrar.utility.enums import ValidationReturnType
@@ -17,157 +20,6 @@ from registrar.utility.enums import ValidationReturnType
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):
generic_org_type = forms.ChoiceField(
# use the long names in the domain request form
@@ -588,28 +440,24 @@ class YourContactForm(RegistrarForm):
)
-class OtherContactsYesNoForm(RegistrarForm):
- def __init__(self, *args, **kwargs):
- """Extend the initialization of the form from RegistrarForm __init__"""
- super().__init__(*args, **kwargs)
- # set the initial value based on attributes of domain request
- if self.domain_request and self.domain_request.has_other_contacts():
- initial_value = True
- elif self.domain_request and self.domain_request.has_rationale():
- initial_value = False
+class OtherContactsYesNoForm(BaseYesNoForm):
+ """The yes/no field for the OtherContacts form."""
+
+ form_choices = ((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)"))
+ field_name = "has_other_contacts"
+
+ @property
+ def form_is_checked(self):
+ """
+ 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:
# No pre-selection for new domain requests
- initial_value = 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.",
- },
- )
+ return None
class OtherContactsForm(RegistrarForm):
@@ -779,7 +627,7 @@ OtherContactsFormSet = forms.formset_factory(
)
-class NoOtherContactsForm(RegistrarForm):
+class NoOtherContactsForm(BaseDeletableRegistrarForm):
no_other_contacts_rationale = forms.CharField(
required=True,
# 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.")},
)
- def __init__(self, *args, **kwargs):
- self.form_data_marked_for_deletion = False
- super().__init__(*args, **kwargs)
- def mark_form_for_deletion(self):
- """Marks no_other_contacts 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 CisaRepresentativeForm(BaseDeletableRegistrarForm):
+ cisa_representative_email = forms.EmailField(
+ required=True,
+ max_length=None,
+ label="Your representative’s email",
+ validators=[
+ MaxLengthValidator(
+ 320,
+ message="Response must be less than 320 characters.",
+ )
+ ],
+ 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."),
+ },
+ )
-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(
- required=False,
+ required=True,
label="Anything else?",
widget=forms.Textarea(),
validators=[
@@ -855,9 +679,22 @@ class AnythingElseForm(RegistrarForm):
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):
is_policy_acknowledged = forms.BooleanField(
label="I read and agree to the requirements for operating a .gov domain.",
diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py
new file mode 100644
index 000000000..2ae50f908
--- /dev/null
+++ b/src/registrar/forms/utility/wizard_form_helper.py
@@ -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
diff --git a/src/registrar/management/commands/populate_domain_updated_federal_agency.py b/src/registrar/management/commands/populate_domain_updated_federal_agency.py
new file mode 100644
index 000000000..dd8ceb3b2
--- /dev/null
+++ b/src/registrar/management/commands/populate_domain_updated_federal_agency.py
@@ -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}"
+ )
diff --git a/src/registrar/management/commands/populate_verification_type.py b/src/registrar/management/commands/populate_verification_type.py
new file mode 100644
index 000000000..b61521977
--- /dev/null
+++ b/src/registrar/management/commands/populate_verification_type.py
@@ -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}"
+ )
diff --git a/src/registrar/management/commands/utility/terminal_helper.py b/src/registrar/management/commands/utility/terminal_helper.py
index b54209750..db3e4a9d3 100644
--- a/src/registrar/management/commands/utility/terminal_helper.py
+++ b/src/registrar/management/commands/utility/terminal_helper.py
@@ -1,5 +1,6 @@
import logging
import sys
+from abc import ABC, abstractmethod
from django.core.paginator import Paginator
from typing import List
from registrar.utility.enums import LogCode
@@ -58,6 +59,55 @@ class ScriptDataHelper:
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:
@staticmethod
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None):
diff --git a/src/registrar/migrations/0087_alter_domain_deleted_alter_domain_expiration_date_and_more.py b/src/registrar/migrations/0087_alter_domain_deleted_alter_domain_expiration_date_and_more.py
new file mode 100644
index 000000000..edbadcb4e
--- /dev/null
+++ b/src/registrar/migrations/0087_alter_domain_deleted_alter_domain_expiration_date_and_more.py
@@ -0,0 +1,1233 @@
+# Generated by Django 4.2.10 on 2024-04-23 15:03
+
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django_fsm
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("registrar", "0086_domaininformation_updated_federal_agency_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="domain",
+ name="deleted",
+ field=models.DateField(
+ editable=False,
+ help_text='Will appear blank unless the domain is in "deleted" state',
+ null=True,
+ verbose_name="deleted on",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domain",
+ name="expiration_date",
+ field=models.DateField(help_text="Date the domain expires in the registry", null=True),
+ ),
+ migrations.AlterField(
+ model_name="domain",
+ name="first_ready",
+ field=models.DateField(
+ editable=False,
+ help_text='Date when this domain first moved into "ready" state; date will never change',
+ null=True,
+ verbose_name="first ready on",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domain",
+ name="state",
+ field=django_fsm.FSMField(
+ choices=[
+ ("unknown", "Unknown"),
+ ("dns needed", "Dns needed"),
+ ("ready", "Ready"),
+ ("on hold", "On hold"),
+ ("deleted", "Deleted"),
+ ],
+ default="unknown",
+ help_text=" ",
+ max_length=21,
+ protected=True,
+ verbose_name="domain state",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="about_your_organization",
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="address_line1",
+ field=models.CharField(blank=True, null=True, verbose_name="address line 1"),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="address_line2",
+ field=models.CharField(blank=True, null=True, verbose_name="address line 2"),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="anything_else",
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="city",
+ field=models.CharField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="creator",
+ field=models.ForeignKey(
+ help_text="Person who submitted the domain request",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="information_created",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="domain",
+ field=models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="domain_info",
+ to="registrar.domain",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="domain_request",
+ field=models.OneToOneField(
+ blank=True,
+ help_text="Request associated with this domain",
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="DomainRequest_info",
+ to="registrar.domainrequest",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="federal_agency",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ (
+ "Administrative Conference of the United States",
+ "Administrative Conference of the United States",
+ ),
+ ("Advisory Council on Historic Preservation", "Advisory Council on Historic Preservation"),
+ ("American Battle Monuments Commission", "American Battle Monuments Commission"),
+ ("AMTRAK", "AMTRAK"),
+ ("Appalachian Regional Commission", "Appalachian Regional Commission"),
+ (
+ "Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
+ "Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
+ ),
+ ("Appraisal Subcommittee", "Appraisal Subcommittee"),
+ ("Architect of the Capitol", "Architect of the Capitol"),
+ ("Armed Forces Retirement Home", "Armed Forces Retirement Home"),
+ (
+ "Barry Goldwater Scholarship and Excellence in Education Foundation",
+ "Barry Goldwater Scholarship and Excellence in Education Foundation",
+ ),
+ (
+ "Barry Goldwater Scholarship and Excellence in Education Program",
+ "Barry Goldwater Scholarship and Excellence in Education Program",
+ ),
+ ("Central Intelligence Agency", "Central Intelligence Agency"),
+ ("Chemical Safety Board", "Chemical Safety Board"),
+ ("Christopher Columbus Fellowship Foundation", "Christopher Columbus Fellowship Foundation"),
+ ("Civil Rights Cold Case Records Review Board", "Civil Rights Cold Case Records Review Board"),
+ (
+ "Commission for the Preservation of America's Heritage Abroad",
+ "Commission for the Preservation of America's Heritage Abroad",
+ ),
+ ("Commission of Fine Arts", "Commission of Fine Arts"),
+ (
+ "Committee for Purchase From People Who Are Blind or Severely Disabled",
+ "Committee for Purchase From People Who Are Blind or Severely Disabled",
+ ),
+ ("Commodity Futures Trading Commission", "Commodity Futures Trading Commission"),
+ ("Congressional Budget Office", "Congressional Budget Office"),
+ ("Consumer Financial Protection Bureau", "Consumer Financial Protection Bureau"),
+ ("Consumer Product Safety Commission", "Consumer Product Safety Commission"),
+ ("Corporation for National & Community Service", "Corporation for National & Community Service"),
+ (
+ "Corporation for National and Community Service",
+ "Corporation for National and Community Service",
+ ),
+ (
+ "Council of Inspectors General on Integrity and Efficiency",
+ "Council of Inspectors General on Integrity and Efficiency",
+ ),
+ ("Court Services and Offender Supervision", "Court Services and Offender Supervision"),
+ ("Cyberspace Solarium Commission", "Cyberspace Solarium Commission"),
+ (
+ "DC Court Services and Offender Supervision Agency",
+ "DC Court Services and Offender Supervision Agency",
+ ),
+ ("DC Pre-trial Services", "DC Pre-trial Services"),
+ ("Defense Nuclear Facilities Safety Board", "Defense Nuclear Facilities Safety Board"),
+ ("Delta Regional Authority", "Delta Regional Authority"),
+ ("Denali Commission", "Denali Commission"),
+ ("Department of Agriculture", "Department of Agriculture"),
+ ("Department of Commerce", "Department of Commerce"),
+ ("Department of Defense", "Department of Defense"),
+ ("Department of Education", "Department of Education"),
+ ("Department of Energy", "Department of Energy"),
+ ("Department of Health and Human Services", "Department of Health and Human Services"),
+ ("Department of Homeland Security", "Department of Homeland Security"),
+ ("Department of Housing and Urban Development", "Department of Housing and Urban Development"),
+ ("Department of Justice", "Department of Justice"),
+ ("Department of Labor", "Department of Labor"),
+ ("Department of State", "Department of State"),
+ ("Department of the Interior", "Department of the Interior"),
+ ("Department of the Treasury", "Department of the Treasury"),
+ ("Department of Transportation", "Department of Transportation"),
+ ("Department of Veterans Affairs", "Department of Veterans Affairs"),
+ ("Director of National Intelligence", "Director of National Intelligence"),
+ ("Dwight D. Eisenhower Memorial Commission", "Dwight D. Eisenhower Memorial Commission"),
+ ("Election Assistance Commission", "Election Assistance Commission"),
+ ("Environmental Protection Agency", "Environmental Protection Agency"),
+ ("Equal Employment Opportunity Commission", "Equal Employment Opportunity Commission"),
+ ("Executive Office of the President", "Executive Office of the President"),
+ ("Export-Import Bank of the United States", "Export-Import Bank of the United States"),
+ ("Export/Import Bank of the U.S.", "Export/Import Bank of the U.S."),
+ ("Farm Credit Administration", "Farm Credit Administration"),
+ ("Farm Credit System Insurance Corporation", "Farm Credit System Insurance Corporation"),
+ ("Federal Communications Commission", "Federal Communications Commission"),
+ ("Federal Deposit Insurance Corporation", "Federal Deposit Insurance Corporation"),
+ ("Federal Election Commission", "Federal Election Commission"),
+ ("Federal Energy Regulatory Commission", "Federal Energy Regulatory Commission"),
+ (
+ "Federal Financial Institutions Examination Council",
+ "Federal Financial Institutions Examination Council",
+ ),
+ ("Federal Housing Finance Agency", "Federal Housing Finance Agency"),
+ ("Federal Judiciary", "Federal Judiciary"),
+ ("Federal Labor Relations Authority", "Federal Labor Relations Authority"),
+ ("Federal Maritime Commission", "Federal Maritime Commission"),
+ ("Federal Mediation and Conciliation Service", "Federal Mediation and Conciliation Service"),
+ (
+ "Federal Mine Safety and Health Review Commission",
+ "Federal Mine Safety and Health Review Commission",
+ ),
+ (
+ "Federal Permitting Improvement Steering Council",
+ "Federal Permitting Improvement Steering Council",
+ ),
+ ("Federal Reserve Board of Governors", "Federal Reserve Board of Governors"),
+ ("Federal Reserve System", "Federal Reserve System"),
+ ("Federal Trade Commission", "Federal Trade Commission"),
+ ("General Services Administration", "General Services Administration"),
+ ("gov Administration", "gov Administration"),
+ ("Government Accountability Office", "Government Accountability Office"),
+ ("Government Publishing Office", "Government Publishing Office"),
+ ("Gulf Coast Ecosystem Restoration Council", "Gulf Coast Ecosystem Restoration Council"),
+ ("Harry S Truman Scholarship Foundation", "Harry S Truman Scholarship Foundation"),
+ ("Harry S. Truman Scholarship Foundation", "Harry S. Truman Scholarship Foundation"),
+ ("Institute of Museum and Library Services", "Institute of Museum and Library Services"),
+ ("Institute of Peace", "Institute of Peace"),
+ ("Inter-American Foundation", "Inter-American Foundation"),
+ (
+ "International Boundary and Water Commission: United States and Mexico",
+ "International Boundary and Water Commission: United States and Mexico",
+ ),
+ (
+ "International Boundary Commission: United States and Canada",
+ "International Boundary Commission: United States and Canada",
+ ),
+ (
+ "International Joint Commission: United States and Canada",
+ "International Joint Commission: United States and Canada",
+ ),
+ ("James Madison Memorial Fellowship Foundation", "James Madison Memorial Fellowship Foundation"),
+ ("Japan-United States Friendship Commission", "Japan-United States Friendship Commission"),
+ ("Japan-US Friendship Commission", "Japan-US Friendship Commission"),
+ ("John F. Kennedy Center for Performing Arts", "John F. Kennedy Center for Performing Arts"),
+ (
+ "John F. Kennedy Center for the Performing Arts",
+ "John F. Kennedy Center for the Performing Arts",
+ ),
+ ("Legal Services Corporation", "Legal Services Corporation"),
+ ("Legislative Branch", "Legislative Branch"),
+ ("Library of Congress", "Library of Congress"),
+ ("Marine Mammal Commission", "Marine Mammal Commission"),
+ (
+ "Medicaid and CHIP Payment and Access Commission",
+ "Medicaid and CHIP Payment and Access Commission",
+ ),
+ ("Medical Payment Advisory Commission", "Medical Payment Advisory Commission"),
+ ("Medicare Payment Advisory Commission", "Medicare Payment Advisory Commission"),
+ ("Merit Systems Protection Board", "Merit Systems Protection Board"),
+ ("Millennium Challenge Corporation", "Millennium Challenge Corporation"),
+ (
+ "Morris K. Udall and Stewart L. Udall Foundation",
+ "Morris K. Udall and Stewart L. Udall Foundation",
+ ),
+ ("National Aeronautics and Space Administration", "National Aeronautics and Space Administration"),
+ ("National Archives and Records Administration", "National Archives and Records Administration"),
+ ("National Capital Planning Commission", "National Capital Planning Commission"),
+ ("National Council on Disability", "National Council on Disability"),
+ ("National Credit Union Administration", "National Credit Union Administration"),
+ ("National Endowment for the Arts", "National Endowment for the Arts"),
+ ("National Endowment for the Humanities", "National Endowment for the Humanities"),
+ (
+ "National Foundation on the Arts and the Humanities",
+ "National Foundation on the Arts and the Humanities",
+ ),
+ ("National Gallery of Art", "National Gallery of Art"),
+ ("National Indian Gaming Commission", "National Indian Gaming Commission"),
+ ("National Labor Relations Board", "National Labor Relations Board"),
+ ("National Mediation Board", "National Mediation Board"),
+ ("National Science Foundation", "National Science Foundation"),
+ (
+ "National Security Commission on Artificial Intelligence",
+ "National Security Commission on Artificial Intelligence",
+ ),
+ ("National Transportation Safety Board", "National Transportation Safety Board"),
+ (
+ "Networking Information Technology Research and Development",
+ "Networking Information Technology Research and Development",
+ ),
+ ("Non-Federal Agency", "Non-Federal Agency"),
+ ("Northern Border Regional Commission", "Northern Border Regional Commission"),
+ ("Nuclear Regulatory Commission", "Nuclear Regulatory Commission"),
+ ("Nuclear Safety Oversight Committee", "Nuclear Safety Oversight Committee"),
+ ("Nuclear Waste Technical Review Board", "Nuclear Waste Technical Review Board"),
+ (
+ "Occupational Safety & Health Review Commission",
+ "Occupational Safety & Health Review Commission",
+ ),
+ (
+ "Occupational Safety and Health Review Commission",
+ "Occupational Safety and Health Review Commission",
+ ),
+ ("Office of Compliance", "Office of Compliance"),
+ ("Office of Congressional Workplace Rights", "Office of Congressional Workplace Rights"),
+ ("Office of Government Ethics", "Office of Government Ethics"),
+ ("Office of Navajo and Hopi Indian Relocation", "Office of Navajo and Hopi Indian Relocation"),
+ ("Office of Personnel Management", "Office of Personnel Management"),
+ ("Open World Leadership Center", "Open World Leadership Center"),
+ ("Overseas Private Investment Corporation", "Overseas Private Investment Corporation"),
+ ("Peace Corps", "Peace Corps"),
+ ("Pension Benefit Guaranty Corporation", "Pension Benefit Guaranty Corporation"),
+ ("Postal Regulatory Commission", "Postal Regulatory Commission"),
+ ("Presidio Trust", "Presidio Trust"),
+ ("Privacy and Civil Liberties Oversight Board", "Privacy and Civil Liberties Oversight Board"),
+ ("Public Buildings Reform Board", "Public Buildings Reform Board"),
+ (
+ "Public Defender Service for the District of Columbia",
+ "Public Defender Service for the District of Columbia",
+ ),
+ ("Railroad Retirement Board", "Railroad Retirement Board"),
+ ("Securities and Exchange Commission", "Securities and Exchange Commission"),
+ ("Selective Service System", "Selective Service System"),
+ ("Small Business Administration", "Small Business Administration"),
+ ("Smithsonian Institution", "Smithsonian Institution"),
+ ("Social Security Administration", "Social Security Administration"),
+ ("Social Security Advisory Board", "Social Security Advisory Board"),
+ ("Southeast Crescent Regional Commission", "Southeast Crescent Regional Commission"),
+ ("Southwest Border Regional Commission", "Southwest Border Regional Commission"),
+ ("State Justice Institute", "State Justice Institute"),
+ ("State, Local, and Tribal Government", "State, Local, and Tribal Government"),
+ ("Stennis Center for Public Service", "Stennis Center for Public Service"),
+ ("Surface Transportation Board", "Surface Transportation Board"),
+ ("Tennessee Valley Authority", "Tennessee Valley Authority"),
+ ("The Executive Office of the President", "The Executive Office of the President"),
+ ("The Intelligence Community", "The Intelligence Community"),
+ ("The Legislative Branch", "The Legislative Branch"),
+ ("The Supreme Court", "The Supreme Court"),
+ (
+ "The United States World War One Centennial Commission",
+ "The United States World War One Centennial Commission",
+ ),
+ ("U.S. Access Board", "U.S. Access Board"),
+ ("U.S. Agency for Global Media", "U.S. Agency for Global Media"),
+ ("U.S. Agency for International Development", "U.S. Agency for International Development"),
+ ("U.S. Capitol Police", "U.S. Capitol Police"),
+ ("U.S. Chemical Safety Board", "U.S. Chemical Safety Board"),
+ (
+ "U.S. China Economic and Security Review Commission",
+ "U.S. China Economic and Security Review Commission",
+ ),
+ (
+ "U.S. Commission for the Preservation of Americas Heritage Abroad",
+ "U.S. Commission for the Preservation of Americas Heritage Abroad",
+ ),
+ ("U.S. Commission of Fine Arts", "U.S. Commission of Fine Arts"),
+ ("U.S. Commission on Civil Rights", "U.S. Commission on Civil Rights"),
+ (
+ "U.S. Commission on International Religious Freedom",
+ "U.S. Commission on International Religious Freedom",
+ ),
+ ("U.S. Courts", "U.S. Courts"),
+ ("U.S. Department of Agriculture", "U.S. Department of Agriculture"),
+ ("U.S. Interagency Council on Homelessness", "U.S. Interagency Council on Homelessness"),
+ ("U.S. International Trade Commission", "U.S. International Trade Commission"),
+ ("U.S. Nuclear Waste Technical Review Board", "U.S. Nuclear Waste Technical Review Board"),
+ ("U.S. Office of Special Counsel", "U.S. Office of Special Counsel"),
+ ("U.S. Peace Corps", "U.S. Peace Corps"),
+ ("U.S. Postal Service", "U.S. Postal Service"),
+ ("U.S. Semiquincentennial Commission", "U.S. Semiquincentennial Commission"),
+ ("U.S. Trade and Development Agency", "U.S. Trade and Development Agency"),
+ (
+ "U.S.-China Economic and Security Review Commission",
+ "U.S.-China Economic and Security Review Commission",
+ ),
+ ("Udall Foundation", "Udall Foundation"),
+ ("United States AbilityOne", "United States AbilityOne"),
+ ("United States Access Board", "United States Access Board"),
+ ("United States African Development Foundation", "United States African Development Foundation"),
+ ("United States Agency for Global Media", "United States Agency for Global Media"),
+ ("United States Arctic Research Commission", "United States Arctic Research Commission"),
+ ("United States Global Change Research Program", "United States Global Change Research Program"),
+ ("United States Holocaust Memorial Museum", "United States Holocaust Memorial Museum"),
+ ("United States Institute of Peace", "United States Institute of Peace"),
+ (
+ "United States Interagency Council on Homelessness",
+ "United States Interagency Council on Homelessness",
+ ),
+ (
+ "United States International Development Finance Corporation",
+ "United States International Development Finance Corporation",
+ ),
+ ("United States International Trade Commission", "United States International Trade Commission"),
+ ("United States Postal Service", "United States Postal Service"),
+ ("United States Senate", "United States Senate"),
+ ("United States Trade and Development Agency", "United States Trade and Development Agency"),
+ (
+ "Utah Reclamation Mitigation and Conservation Commission",
+ "Utah Reclamation Mitigation and Conservation Commission",
+ ),
+ ("Vietnam Education Foundation", "Vietnam Education Foundation"),
+ ("Western Hemisphere Drug Policy Commission", "Western Hemisphere Drug Policy Commission"),
+ (
+ "Woodrow Wilson International Center for Scholars",
+ "Woodrow Wilson International Center for Scholars",
+ ),
+ ("World War I Centennial Commission", "World War I Centennial Commission"),
+ ],
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="federal_type",
+ field=models.CharField(
+ blank=True,
+ choices=[("executive", "Executive"), ("judicial", "Judicial"), ("legislative", "Legislative")],
+ max_length=50,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="federally_recognized_tribe",
+ field=models.BooleanField(null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="is_election_board",
+ field=models.BooleanField(blank=True, null=True, verbose_name="election office"),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="no_other_contacts_rationale",
+ field=models.TextField(
+ blank=True, help_text="Required if creator does not list other employees", null=True
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="notes",
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="organization_name",
+ field=models.CharField(blank=True, db_index=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="organization_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ("state_or_territory_election", "State or territory - Election"),
+ ("tribal_election", "Tribal - Election"),
+ ("county_election", "County - Election"),
+ ("city_election", "City - Election"),
+ ("special_district_election", "Special district - Election"),
+ ],
+ help_text='"Election" appears after the org type if it\'s an election office.',
+ max_length=255,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="state_recognized_tribe",
+ field=models.BooleanField(null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="state_territory",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("AL", "Alabama (AL)"),
+ ("AK", "Alaska (AK)"),
+ ("AS", "American Samoa (AS)"),
+ ("AZ", "Arizona (AZ)"),
+ ("AR", "Arkansas (AR)"),
+ ("CA", "California (CA)"),
+ ("CO", "Colorado (CO)"),
+ ("CT", "Connecticut (CT)"),
+ ("DE", "Delaware (DE)"),
+ ("DC", "District of Columbia (DC)"),
+ ("FL", "Florida (FL)"),
+ ("GA", "Georgia (GA)"),
+ ("GU", "Guam (GU)"),
+ ("HI", "Hawaii (HI)"),
+ ("ID", "Idaho (ID)"),
+ ("IL", "Illinois (IL)"),
+ ("IN", "Indiana (IN)"),
+ ("IA", "Iowa (IA)"),
+ ("KS", "Kansas (KS)"),
+ ("KY", "Kentucky (KY)"),
+ ("LA", "Louisiana (LA)"),
+ ("ME", "Maine (ME)"),
+ ("MD", "Maryland (MD)"),
+ ("MA", "Massachusetts (MA)"),
+ ("MI", "Michigan (MI)"),
+ ("MN", "Minnesota (MN)"),
+ ("MS", "Mississippi (MS)"),
+ ("MO", "Missouri (MO)"),
+ ("MT", "Montana (MT)"),
+ ("NE", "Nebraska (NE)"),
+ ("NV", "Nevada (NV)"),
+ ("NH", "New Hampshire (NH)"),
+ ("NJ", "New Jersey (NJ)"),
+ ("NM", "New Mexico (NM)"),
+ ("NY", "New York (NY)"),
+ ("NC", "North Carolina (NC)"),
+ ("ND", "North Dakota (ND)"),
+ ("MP", "Northern Mariana Islands (MP)"),
+ ("OH", "Ohio (OH)"),
+ ("OK", "Oklahoma (OK)"),
+ ("OR", "Oregon (OR)"),
+ ("PA", "Pennsylvania (PA)"),
+ ("PR", "Puerto Rico (PR)"),
+ ("RI", "Rhode Island (RI)"),
+ ("SC", "South Carolina (SC)"),
+ ("SD", "South Dakota (SD)"),
+ ("TN", "Tennessee (TN)"),
+ ("TX", "Texas (TX)"),
+ ("UM", "United States Minor Outlying Islands (UM)"),
+ ("UT", "Utah (UT)"),
+ ("VT", "Vermont (VT)"),
+ ("VI", "Virgin Islands (VI)"),
+ ("VA", "Virginia (VA)"),
+ ("WA", "Washington (WA)"),
+ ("WV", "West Virginia (WV)"),
+ ("WI", "Wisconsin (WI)"),
+ ("WY", "Wyoming (WY)"),
+ ("AA", "Armed Forces Americas (AA)"),
+ ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
+ ("AP", "Armed Forces Pacific (AP)"),
+ ],
+ max_length=2,
+ null=True,
+ verbose_name="state / territory",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="submitter",
+ field=models.ForeignKey(
+ blank=True,
+ help_text='Person listed under "your contact information" in the request form',
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="submitted_domain_requests_information",
+ to="registrar.contact",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="tribe_name",
+ field=models.CharField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="urbanization",
+ field=models.CharField(
+ blank=True, help_text="Required for Puerto Rico only", null=True, verbose_name="urbanization"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="zipcode",
+ field=models.CharField(blank=True, db_index=True, max_length=10, null=True, verbose_name="zip code"),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="about_your_organization",
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="address_line1",
+ field=models.CharField(blank=True, null=True, verbose_name="Address line 1"),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="address_line2",
+ field=models.CharField(blank=True, null=True, verbose_name="Address line 2"),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="alternative_domains",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Other domain names the creator provided for consideration",
+ related_name="alternatives+",
+ to="registrar.website",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="anything_else",
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="approved_domain",
+ field=models.OneToOneField(
+ blank=True,
+ help_text="Domain associated with this request; will be blank until request is approved",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="domain_request",
+ to="registrar.domain",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="city",
+ field=models.CharField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="creator",
+ field=models.ForeignKey(
+ help_text="Person who submitted the domain request; will not receive email updates",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="domain_requests_created",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="federal_agency",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ (
+ "Administrative Conference of the United States",
+ "Administrative Conference of the United States",
+ ),
+ ("Advisory Council on Historic Preservation", "Advisory Council on Historic Preservation"),
+ ("American Battle Monuments Commission", "American Battle Monuments Commission"),
+ ("AMTRAK", "AMTRAK"),
+ ("Appalachian Regional Commission", "Appalachian Regional Commission"),
+ (
+ "Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
+ "Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
+ ),
+ ("Appraisal Subcommittee", "Appraisal Subcommittee"),
+ ("Architect of the Capitol", "Architect of the Capitol"),
+ ("Armed Forces Retirement Home", "Armed Forces Retirement Home"),
+ (
+ "Barry Goldwater Scholarship and Excellence in Education Foundation",
+ "Barry Goldwater Scholarship and Excellence in Education Foundation",
+ ),
+ (
+ "Barry Goldwater Scholarship and Excellence in Education Program",
+ "Barry Goldwater Scholarship and Excellence in Education Program",
+ ),
+ ("Central Intelligence Agency", "Central Intelligence Agency"),
+ ("Chemical Safety Board", "Chemical Safety Board"),
+ ("Christopher Columbus Fellowship Foundation", "Christopher Columbus Fellowship Foundation"),
+ ("Civil Rights Cold Case Records Review Board", "Civil Rights Cold Case Records Review Board"),
+ (
+ "Commission for the Preservation of America's Heritage Abroad",
+ "Commission for the Preservation of America's Heritage Abroad",
+ ),
+ ("Commission of Fine Arts", "Commission of Fine Arts"),
+ (
+ "Committee for Purchase From People Who Are Blind or Severely Disabled",
+ "Committee for Purchase From People Who Are Blind or Severely Disabled",
+ ),
+ ("Commodity Futures Trading Commission", "Commodity Futures Trading Commission"),
+ ("Congressional Budget Office", "Congressional Budget Office"),
+ ("Consumer Financial Protection Bureau", "Consumer Financial Protection Bureau"),
+ ("Consumer Product Safety Commission", "Consumer Product Safety Commission"),
+ ("Corporation for National & Community Service", "Corporation for National & Community Service"),
+ (
+ "Corporation for National and Community Service",
+ "Corporation for National and Community Service",
+ ),
+ (
+ "Council of Inspectors General on Integrity and Efficiency",
+ "Council of Inspectors General on Integrity and Efficiency",
+ ),
+ ("Court Services and Offender Supervision", "Court Services and Offender Supervision"),
+ ("Cyberspace Solarium Commission", "Cyberspace Solarium Commission"),
+ (
+ "DC Court Services and Offender Supervision Agency",
+ "DC Court Services and Offender Supervision Agency",
+ ),
+ ("DC Pre-trial Services", "DC Pre-trial Services"),
+ ("Defense Nuclear Facilities Safety Board", "Defense Nuclear Facilities Safety Board"),
+ ("Delta Regional Authority", "Delta Regional Authority"),
+ ("Denali Commission", "Denali Commission"),
+ ("Department of Agriculture", "Department of Agriculture"),
+ ("Department of Commerce", "Department of Commerce"),
+ ("Department of Defense", "Department of Defense"),
+ ("Department of Education", "Department of Education"),
+ ("Department of Energy", "Department of Energy"),
+ ("Department of Health and Human Services", "Department of Health and Human Services"),
+ ("Department of Homeland Security", "Department of Homeland Security"),
+ ("Department of Housing and Urban Development", "Department of Housing and Urban Development"),
+ ("Department of Justice", "Department of Justice"),
+ ("Department of Labor", "Department of Labor"),
+ ("Department of State", "Department of State"),
+ ("Department of the Interior", "Department of the Interior"),
+ ("Department of the Treasury", "Department of the Treasury"),
+ ("Department of Transportation", "Department of Transportation"),
+ ("Department of Veterans Affairs", "Department of Veterans Affairs"),
+ ("Director of National Intelligence", "Director of National Intelligence"),
+ ("Dwight D. Eisenhower Memorial Commission", "Dwight D. Eisenhower Memorial Commission"),
+ ("Election Assistance Commission", "Election Assistance Commission"),
+ ("Environmental Protection Agency", "Environmental Protection Agency"),
+ ("Equal Employment Opportunity Commission", "Equal Employment Opportunity Commission"),
+ ("Executive Office of the President", "Executive Office of the President"),
+ ("Export-Import Bank of the United States", "Export-Import Bank of the United States"),
+ ("Export/Import Bank of the U.S.", "Export/Import Bank of the U.S."),
+ ("Farm Credit Administration", "Farm Credit Administration"),
+ ("Farm Credit System Insurance Corporation", "Farm Credit System Insurance Corporation"),
+ ("Federal Communications Commission", "Federal Communications Commission"),
+ ("Federal Deposit Insurance Corporation", "Federal Deposit Insurance Corporation"),
+ ("Federal Election Commission", "Federal Election Commission"),
+ ("Federal Energy Regulatory Commission", "Federal Energy Regulatory Commission"),
+ (
+ "Federal Financial Institutions Examination Council",
+ "Federal Financial Institutions Examination Council",
+ ),
+ ("Federal Housing Finance Agency", "Federal Housing Finance Agency"),
+ ("Federal Judiciary", "Federal Judiciary"),
+ ("Federal Labor Relations Authority", "Federal Labor Relations Authority"),
+ ("Federal Maritime Commission", "Federal Maritime Commission"),
+ ("Federal Mediation and Conciliation Service", "Federal Mediation and Conciliation Service"),
+ (
+ "Federal Mine Safety and Health Review Commission",
+ "Federal Mine Safety and Health Review Commission",
+ ),
+ (
+ "Federal Permitting Improvement Steering Council",
+ "Federal Permitting Improvement Steering Council",
+ ),
+ ("Federal Reserve Board of Governors", "Federal Reserve Board of Governors"),
+ ("Federal Reserve System", "Federal Reserve System"),
+ ("Federal Trade Commission", "Federal Trade Commission"),
+ ("General Services Administration", "General Services Administration"),
+ ("gov Administration", "gov Administration"),
+ ("Government Accountability Office", "Government Accountability Office"),
+ ("Government Publishing Office", "Government Publishing Office"),
+ ("Gulf Coast Ecosystem Restoration Council", "Gulf Coast Ecosystem Restoration Council"),
+ ("Harry S Truman Scholarship Foundation", "Harry S Truman Scholarship Foundation"),
+ ("Harry S. Truman Scholarship Foundation", "Harry S. Truman Scholarship Foundation"),
+ ("Institute of Museum and Library Services", "Institute of Museum and Library Services"),
+ ("Institute of Peace", "Institute of Peace"),
+ ("Inter-American Foundation", "Inter-American Foundation"),
+ (
+ "International Boundary and Water Commission: United States and Mexico",
+ "International Boundary and Water Commission: United States and Mexico",
+ ),
+ (
+ "International Boundary Commission: United States and Canada",
+ "International Boundary Commission: United States and Canada",
+ ),
+ (
+ "International Joint Commission: United States and Canada",
+ "International Joint Commission: United States and Canada",
+ ),
+ ("James Madison Memorial Fellowship Foundation", "James Madison Memorial Fellowship Foundation"),
+ ("Japan-United States Friendship Commission", "Japan-United States Friendship Commission"),
+ ("Japan-US Friendship Commission", "Japan-US Friendship Commission"),
+ ("John F. Kennedy Center for Performing Arts", "John F. Kennedy Center for Performing Arts"),
+ (
+ "John F. Kennedy Center for the Performing Arts",
+ "John F. Kennedy Center for the Performing Arts",
+ ),
+ ("Legal Services Corporation", "Legal Services Corporation"),
+ ("Legislative Branch", "Legislative Branch"),
+ ("Library of Congress", "Library of Congress"),
+ ("Marine Mammal Commission", "Marine Mammal Commission"),
+ (
+ "Medicaid and CHIP Payment and Access Commission",
+ "Medicaid and CHIP Payment and Access Commission",
+ ),
+ ("Medical Payment Advisory Commission", "Medical Payment Advisory Commission"),
+ ("Medicare Payment Advisory Commission", "Medicare Payment Advisory Commission"),
+ ("Merit Systems Protection Board", "Merit Systems Protection Board"),
+ ("Millennium Challenge Corporation", "Millennium Challenge Corporation"),
+ (
+ "Morris K. Udall and Stewart L. Udall Foundation",
+ "Morris K. Udall and Stewart L. Udall Foundation",
+ ),
+ ("National Aeronautics and Space Administration", "National Aeronautics and Space Administration"),
+ ("National Archives and Records Administration", "National Archives and Records Administration"),
+ ("National Capital Planning Commission", "National Capital Planning Commission"),
+ ("National Council on Disability", "National Council on Disability"),
+ ("National Credit Union Administration", "National Credit Union Administration"),
+ ("National Endowment for the Arts", "National Endowment for the Arts"),
+ ("National Endowment for the Humanities", "National Endowment for the Humanities"),
+ (
+ "National Foundation on the Arts and the Humanities",
+ "National Foundation on the Arts and the Humanities",
+ ),
+ ("National Gallery of Art", "National Gallery of Art"),
+ ("National Indian Gaming Commission", "National Indian Gaming Commission"),
+ ("National Labor Relations Board", "National Labor Relations Board"),
+ ("National Mediation Board", "National Mediation Board"),
+ ("National Science Foundation", "National Science Foundation"),
+ (
+ "National Security Commission on Artificial Intelligence",
+ "National Security Commission on Artificial Intelligence",
+ ),
+ ("National Transportation Safety Board", "National Transportation Safety Board"),
+ (
+ "Networking Information Technology Research and Development",
+ "Networking Information Technology Research and Development",
+ ),
+ ("Non-Federal Agency", "Non-Federal Agency"),
+ ("Northern Border Regional Commission", "Northern Border Regional Commission"),
+ ("Nuclear Regulatory Commission", "Nuclear Regulatory Commission"),
+ ("Nuclear Safety Oversight Committee", "Nuclear Safety Oversight Committee"),
+ ("Nuclear Waste Technical Review Board", "Nuclear Waste Technical Review Board"),
+ (
+ "Occupational Safety & Health Review Commission",
+ "Occupational Safety & Health Review Commission",
+ ),
+ (
+ "Occupational Safety and Health Review Commission",
+ "Occupational Safety and Health Review Commission",
+ ),
+ ("Office of Compliance", "Office of Compliance"),
+ ("Office of Congressional Workplace Rights", "Office of Congressional Workplace Rights"),
+ ("Office of Government Ethics", "Office of Government Ethics"),
+ ("Office of Navajo and Hopi Indian Relocation", "Office of Navajo and Hopi Indian Relocation"),
+ ("Office of Personnel Management", "Office of Personnel Management"),
+ ("Open World Leadership Center", "Open World Leadership Center"),
+ ("Overseas Private Investment Corporation", "Overseas Private Investment Corporation"),
+ ("Peace Corps", "Peace Corps"),
+ ("Pension Benefit Guaranty Corporation", "Pension Benefit Guaranty Corporation"),
+ ("Postal Regulatory Commission", "Postal Regulatory Commission"),
+ ("Presidio Trust", "Presidio Trust"),
+ ("Privacy and Civil Liberties Oversight Board", "Privacy and Civil Liberties Oversight Board"),
+ ("Public Buildings Reform Board", "Public Buildings Reform Board"),
+ (
+ "Public Defender Service for the District of Columbia",
+ "Public Defender Service for the District of Columbia",
+ ),
+ ("Railroad Retirement Board", "Railroad Retirement Board"),
+ ("Securities and Exchange Commission", "Securities and Exchange Commission"),
+ ("Selective Service System", "Selective Service System"),
+ ("Small Business Administration", "Small Business Administration"),
+ ("Smithsonian Institution", "Smithsonian Institution"),
+ ("Social Security Administration", "Social Security Administration"),
+ ("Social Security Advisory Board", "Social Security Advisory Board"),
+ ("Southeast Crescent Regional Commission", "Southeast Crescent Regional Commission"),
+ ("Southwest Border Regional Commission", "Southwest Border Regional Commission"),
+ ("State Justice Institute", "State Justice Institute"),
+ ("State, Local, and Tribal Government", "State, Local, and Tribal Government"),
+ ("Stennis Center for Public Service", "Stennis Center for Public Service"),
+ ("Surface Transportation Board", "Surface Transportation Board"),
+ ("Tennessee Valley Authority", "Tennessee Valley Authority"),
+ ("The Executive Office of the President", "The Executive Office of the President"),
+ ("The Intelligence Community", "The Intelligence Community"),
+ ("The Legislative Branch", "The Legislative Branch"),
+ ("The Supreme Court", "The Supreme Court"),
+ (
+ "The United States World War One Centennial Commission",
+ "The United States World War One Centennial Commission",
+ ),
+ ("U.S. Access Board", "U.S. Access Board"),
+ ("U.S. Agency for Global Media", "U.S. Agency for Global Media"),
+ ("U.S. Agency for International Development", "U.S. Agency for International Development"),
+ ("U.S. Capitol Police", "U.S. Capitol Police"),
+ ("U.S. Chemical Safety Board", "U.S. Chemical Safety Board"),
+ (
+ "U.S. China Economic and Security Review Commission",
+ "U.S. China Economic and Security Review Commission",
+ ),
+ (
+ "U.S. Commission for the Preservation of Americas Heritage Abroad",
+ "U.S. Commission for the Preservation of Americas Heritage Abroad",
+ ),
+ ("U.S. Commission of Fine Arts", "U.S. Commission of Fine Arts"),
+ ("U.S. Commission on Civil Rights", "U.S. Commission on Civil Rights"),
+ (
+ "U.S. Commission on International Religious Freedom",
+ "U.S. Commission on International Religious Freedom",
+ ),
+ ("U.S. Courts", "U.S. Courts"),
+ ("U.S. Department of Agriculture", "U.S. Department of Agriculture"),
+ ("U.S. Interagency Council on Homelessness", "U.S. Interagency Council on Homelessness"),
+ ("U.S. International Trade Commission", "U.S. International Trade Commission"),
+ ("U.S. Nuclear Waste Technical Review Board", "U.S. Nuclear Waste Technical Review Board"),
+ ("U.S. Office of Special Counsel", "U.S. Office of Special Counsel"),
+ ("U.S. Peace Corps", "U.S. Peace Corps"),
+ ("U.S. Postal Service", "U.S. Postal Service"),
+ ("U.S. Semiquincentennial Commission", "U.S. Semiquincentennial Commission"),
+ ("U.S. Trade and Development Agency", "U.S. Trade and Development Agency"),
+ (
+ "U.S.-China Economic and Security Review Commission",
+ "U.S.-China Economic and Security Review Commission",
+ ),
+ ("Udall Foundation", "Udall Foundation"),
+ ("United States AbilityOne", "United States AbilityOne"),
+ ("United States Access Board", "United States Access Board"),
+ ("United States African Development Foundation", "United States African Development Foundation"),
+ ("United States Agency for Global Media", "United States Agency for Global Media"),
+ ("United States Arctic Research Commission", "United States Arctic Research Commission"),
+ ("United States Global Change Research Program", "United States Global Change Research Program"),
+ ("United States Holocaust Memorial Museum", "United States Holocaust Memorial Museum"),
+ ("United States Institute of Peace", "United States Institute of Peace"),
+ (
+ "United States Interagency Council on Homelessness",
+ "United States Interagency Council on Homelessness",
+ ),
+ (
+ "United States International Development Finance Corporation",
+ "United States International Development Finance Corporation",
+ ),
+ ("United States International Trade Commission", "United States International Trade Commission"),
+ ("United States Postal Service", "United States Postal Service"),
+ ("United States Senate", "United States Senate"),
+ ("United States Trade and Development Agency", "United States Trade and Development Agency"),
+ (
+ "Utah Reclamation Mitigation and Conservation Commission",
+ "Utah Reclamation Mitigation and Conservation Commission",
+ ),
+ ("Vietnam Education Foundation", "Vietnam Education Foundation"),
+ ("Western Hemisphere Drug Policy Commission", "Western Hemisphere Drug Policy Commission"),
+ (
+ "Woodrow Wilson International Center for Scholars",
+ "Woodrow Wilson International Center for Scholars",
+ ),
+ ("World War I Centennial Commission", "World War I Centennial Commission"),
+ ],
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="federal_type",
+ field=models.CharField(
+ blank=True,
+ choices=[("executive", "Executive"), ("judicial", "Judicial"), ("legislative", "Legislative")],
+ max_length=50,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="federally_recognized_tribe",
+ field=models.BooleanField(null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="generic_org_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ],
+ max_length=255,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="is_election_board",
+ field=models.BooleanField(blank=True, null=True, verbose_name="election office"),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="no_other_contacts_rationale",
+ field=models.TextField(
+ blank=True, help_text="Required if creator does not list other employees", null=True
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="notes",
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="organization_name",
+ field=models.CharField(blank=True, db_index=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="organization_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ("state_or_territory_election", "State or territory - Election"),
+ ("tribal_election", "Tribal - Election"),
+ ("county_election", "County - Election"),
+ ("city_election", "City - Election"),
+ ("special_district_election", "Special district - Election"),
+ ],
+ help_text='"Election" appears after the org type if it\'s an election office.',
+ max_length=255,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="purpose",
+ field=models.TextField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="requested_domain",
+ field=models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="domain_request",
+ to="registrar.draftdomain",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="state_recognized_tribe",
+ field=models.BooleanField(null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="state_territory",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("AL", "Alabama (AL)"),
+ ("AK", "Alaska (AK)"),
+ ("AS", "American Samoa (AS)"),
+ ("AZ", "Arizona (AZ)"),
+ ("AR", "Arkansas (AR)"),
+ ("CA", "California (CA)"),
+ ("CO", "Colorado (CO)"),
+ ("CT", "Connecticut (CT)"),
+ ("DE", "Delaware (DE)"),
+ ("DC", "District of Columbia (DC)"),
+ ("FL", "Florida (FL)"),
+ ("GA", "Georgia (GA)"),
+ ("GU", "Guam (GU)"),
+ ("HI", "Hawaii (HI)"),
+ ("ID", "Idaho (ID)"),
+ ("IL", "Illinois (IL)"),
+ ("IN", "Indiana (IN)"),
+ ("IA", "Iowa (IA)"),
+ ("KS", "Kansas (KS)"),
+ ("KY", "Kentucky (KY)"),
+ ("LA", "Louisiana (LA)"),
+ ("ME", "Maine (ME)"),
+ ("MD", "Maryland (MD)"),
+ ("MA", "Massachusetts (MA)"),
+ ("MI", "Michigan (MI)"),
+ ("MN", "Minnesota (MN)"),
+ ("MS", "Mississippi (MS)"),
+ ("MO", "Missouri (MO)"),
+ ("MT", "Montana (MT)"),
+ ("NE", "Nebraska (NE)"),
+ ("NV", "Nevada (NV)"),
+ ("NH", "New Hampshire (NH)"),
+ ("NJ", "New Jersey (NJ)"),
+ ("NM", "New Mexico (NM)"),
+ ("NY", "New York (NY)"),
+ ("NC", "North Carolina (NC)"),
+ ("ND", "North Dakota (ND)"),
+ ("MP", "Northern Mariana Islands (MP)"),
+ ("OH", "Ohio (OH)"),
+ ("OK", "Oklahoma (OK)"),
+ ("OR", "Oregon (OR)"),
+ ("PA", "Pennsylvania (PA)"),
+ ("PR", "Puerto Rico (PR)"),
+ ("RI", "Rhode Island (RI)"),
+ ("SC", "South Carolina (SC)"),
+ ("SD", "South Dakota (SD)"),
+ ("TN", "Tennessee (TN)"),
+ ("TX", "Texas (TX)"),
+ ("UM", "United States Minor Outlying Islands (UM)"),
+ ("UT", "Utah (UT)"),
+ ("VT", "Vermont (VT)"),
+ ("VI", "Virgin Islands (VI)"),
+ ("VA", "Virginia (VA)"),
+ ("WA", "Washington (WA)"),
+ ("WV", "West Virginia (WV)"),
+ ("WI", "Wisconsin (WI)"),
+ ("WY", "Wyoming (WY)"),
+ ("AA", "Armed Forces Americas (AA)"),
+ ("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
+ ("AP", "Armed Forces Pacific (AP)"),
+ ],
+ max_length=2,
+ null=True,
+ verbose_name="state / territory",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="submitter",
+ field=models.ForeignKey(
+ blank=True,
+ help_text='Person listed under "your contact information" in the request form; will receive email updates',
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="submitted_domain_requests",
+ to="registrar.contact",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="tribe_name",
+ field=models.CharField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="urbanization",
+ field=models.CharField(blank=True, help_text="Required for Puerto Rico only", null=True),
+ ),
+ migrations.AlterField(
+ model_name="domainrequest",
+ name="zipcode",
+ field=models.CharField(blank=True, db_index=True, max_length=10, null=True, verbose_name="zip code"),
+ ),
+ migrations.AlterField(
+ model_name="host",
+ name="domain",
+ field=models.ForeignKey(
+ help_text="Domain associated with this host",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="host",
+ to="registrar.domain",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="host",
+ name="name",
+ field=models.CharField(default=None, max_length=253, verbose_name="host name"),
+ ),
+ migrations.AlterField(
+ model_name="hostip",
+ name="address",
+ field=models.CharField(
+ default=None,
+ max_length=46,
+ validators=[django.core.validators.validate_ipv46_address],
+ verbose_name="IP address",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="hostip",
+ name="host",
+ field=models.ForeignKey(
+ help_text="IP associated with this host",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="ip",
+ to="registrar.host",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="user",
+ name="status",
+ field=models.CharField(
+ blank=True,
+ choices=[("restricted", "restricted")],
+ default=None,
+ help_text='Users in "restricted" status cannot make updates in the registrar or start a new request.',
+ max_length=10,
+ null=True,
+ verbose_name="user status",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="verifiedbystaff",
+ name="email",
+ field=models.EmailField(db_index=True, max_length=254),
+ ),
+ migrations.AlterField(
+ model_name="verifiedbystaff",
+ name="notes",
+ field=models.TextField(),
+ ),
+ migrations.AlterField(
+ model_name="verifiedbystaff",
+ name="requestor",
+ field=models.ForeignKey(
+ blank=True,
+ help_text="Person who verified this user",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="verifiedby_user",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="website",
+ name="website",
+ field=models.CharField(
+ help_text="An alternative domain or current website listed on a domain request", max_length=255
+ ),
+ ),
+ ]
diff --git a/src/registrar/migrations/0088_domaininformation_cisa_representative_email_and_more.py b/src/registrar/migrations/0088_domaininformation_cisa_representative_email_and_more.py
new file mode 100644
index 000000000..95450fb3d
--- /dev/null
+++ b/src/registrar/migrations/0088_domaininformation_cisa_representative_email_and_more.py
@@ -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"),
+ ),
+ ]
diff --git a/src/registrar/migrations/0089_user_verification_type.py b/src/registrar/migrations/0089_user_verification_type.py
new file mode 100644
index 000000000..e021e89e1
--- /dev/null
+++ b/src/registrar/migrations/0089_user_verification_type.py
@@ -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,
+ ),
+ ),
+ ]
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 0d0d8020d..7f53bb234 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -159,6 +159,31 @@ class Domain(TimeStampedModel, DomainHelper):
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):
"""
Python descriptor to turn class methods into properties.
@@ -992,22 +1017,25 @@ class Domain(TimeStampedModel, DomainHelper):
blank=False,
default=None, # prevent saving without a value
unique=True,
- verbose_name="domain",
help_text="Fully qualified domain name",
+ verbose_name="domain",
)
state = FSMField(
max_length=21,
choices=State.choices,
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",
- help_text="Very basic info about the lifecycle of this domain object",
)
expiration_date = DateField(
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(
@@ -1019,15 +1047,15 @@ class Domain(TimeStampedModel, DomainHelper):
deleted = DateField(
null=True,
editable=False,
+ help_text='Will appear blank unless the domain is in "deleted" state',
verbose_name="deleted on",
- help_text="Deleted at date",
)
first_ready = DateField(
null=True,
editable=False,
+ help_text='Date when this domain first moved into "ready" state; date will never change',
verbose_name="first ready on",
- help_text="The last time this domain moved into the READY state",
)
def isActive(self):
diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py
index 3c206457e..c724423ce 100644
--- a/src/registrar/models/domain_information.py
+++ b/src/registrar/models/domain_information.py
@@ -47,6 +47,7 @@ class DomainInformation(TimeStampedModel):
"registrar.User",
on_delete=models.PROTECT,
related_name="information_created",
+ help_text="Person who submitted the domain request",
)
domain_request = models.OneToOneField(
@@ -55,7 +56,7 @@ class DomainInformation(TimeStampedModel):
blank=True,
null=True,
related_name="DomainRequest_info",
- help_text="Associated domain request",
+ help_text="Request associated with this domain",
unique=True,
)
@@ -73,7 +74,6 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
verbose_name="election office",
- help_text="Is your organization an election office?",
)
# TODO - Ticket #1911: stub this data from DomainRequest
@@ -82,30 +82,26 @@ class DomainInformation(TimeStampedModel):
choices=DomainRequest.OrgChoicesElectionOffice.choices,
null=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(
null=True,
- help_text="Is the tribe federally recognized",
)
state_recognized_tribe = models.BooleanField(
null=True,
- help_text="Is the tribe recognized by a state",
)
tribe_name = models.CharField(
null=True,
blank=True,
- help_text="Name of tribe",
)
federal_agency = models.CharField(
choices=AGENCY_CHOICES,
null=True,
blank=True,
- help_text="Federal agency",
)
federal_type = models.CharField(
@@ -113,38 +109,32 @@ class DomainInformation(TimeStampedModel):
choices=BranchChoices.choices,
null=True,
blank=True,
- help_text="Federal government branch",
)
is_election_board = models.BooleanField(
null=True,
blank=True,
verbose_name="election office",
- help_text="Is your organization an election office?",
)
organization_name = models.CharField(
null=True,
blank=True,
- help_text="Organization name",
db_index=True,
)
address_line1 = models.CharField(
null=True,
blank=True,
- help_text="Street address",
verbose_name="address line 1",
)
address_line2 = models.CharField(
null=True,
blank=True,
- help_text="Street address line 2 (optional)",
verbose_name="address line 2",
)
city = models.CharField(
null=True,
blank=True,
- help_text="City",
)
state_territory = models.CharField(
max_length=2,
@@ -152,27 +142,24 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
verbose_name="state / territory",
- help_text="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
- help_text="Zip code",
- verbose_name="zip code",
db_index=True,
+ verbose_name="zip code",
)
urbanization = models.CharField(
null=True,
blank=True,
- help_text="Urbanization (required for Puerto Rico only)",
+ help_text="Required for Puerto Rico only",
verbose_name="urbanization",
)
about_your_organization = models.TextField(
null=True,
blank=True,
- help_text="Information about your organization",
)
authorizing_official = models.ForeignKey(
@@ -190,7 +177,6 @@ class DomainInformation(TimeStampedModel):
null=True,
# Access this information via Domain as "domain.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
@@ -201,6 +187,7 @@ class DomainInformation(TimeStampedModel):
blank=True,
related_name="submitted_domain_requests_information",
on_delete=models.PROTECT,
+ help_text='Person listed under "your contact information" in the request form',
)
purpose = models.TextField(
@@ -219,13 +206,20 @@ class DomainInformation(TimeStampedModel):
no_other_contacts_rationale = models.TextField(
null=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(
null=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(
@@ -237,7 +231,6 @@ class DomainInformation(TimeStampedModel):
notes = models.TextField(
null=True,
blank=True,
- help_text="Notes about the request",
)
def __str__(self):
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 5b8fc59e3..75fbadc3e 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -16,12 +16,21 @@ from .utility.time_stamped_model import TimeStampedModel
from ..utility.email import send_templated_email, EmailSendingError
from itertools import chain
+from auditlog.models import AuditlogHistoryField # type: ignore
+
logger = logging.getLogger(__name__)
class DomainRequest(TimeStampedModel):
"""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
class DomainRequestStatus(models.TextChoices):
STARTED = "started", "Started"
@@ -464,6 +473,7 @@ class DomainRequest(TimeStampedModel):
"registrar.User",
on_delete=models.PROTECT,
related_name="domain_requests_created",
+ help_text="Person who submitted the domain request; will not receive email updates",
)
investigator = models.ForeignKey(
@@ -481,14 +491,12 @@ class DomainRequest(TimeStampedModel):
choices=OrganizationChoices.choices,
null=True,
blank=True,
- help_text="Type of organization",
)
is_election_board = models.BooleanField(
null=True,
blank=True,
verbose_name="election office",
- help_text="Is your organization an election office?",
)
# TODO - Ticket #1911: stub this data from DomainRequest
@@ -497,30 +505,26 @@ class DomainRequest(TimeStampedModel):
choices=OrgChoicesElectionOffice.choices,
null=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(
null=True,
- help_text="Is the tribe federally recognized",
)
state_recognized_tribe = models.BooleanField(
null=True,
- help_text="Is the tribe recognized by a state",
)
tribe_name = models.CharField(
null=True,
blank=True,
- help_text="Name of tribe",
)
federal_agency = models.CharField(
choices=AGENCY_CHOICES,
null=True,
blank=True,
- help_text="Federal agency",
)
federal_type = models.CharField(
@@ -528,32 +532,27 @@ class DomainRequest(TimeStampedModel):
choices=BranchChoices.choices,
null=True,
blank=True,
- help_text="Federal government branch",
)
organization_name = models.CharField(
null=True,
blank=True,
- help_text="Organization name",
db_index=True,
)
address_line1 = models.CharField(
null=True,
blank=True,
- help_text="Street address",
verbose_name="Address line 1",
)
address_line2 = models.CharField(
null=True,
blank=True,
- help_text="Street address line 2 (optional)",
verbose_name="Address line 2",
)
city = models.CharField(
null=True,
blank=True,
- help_text="City",
)
state_territory = models.CharField(
max_length=2,
@@ -561,26 +560,23 @@ class DomainRequest(TimeStampedModel):
null=True,
blank=True,
verbose_name="state / territory",
- help_text="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
verbose_name="zip code",
- help_text="Zip code",
db_index=True,
)
urbanization = models.CharField(
null=True,
blank=True,
- help_text="Urbanization (required for Puerto Rico only)",
+ help_text="Required for Puerto Rico only",
)
about_your_organization = models.TextField(
null=True,
blank=True,
- help_text="Information about your organization",
)
authorizing_official = models.ForeignKey(
@@ -603,7 +599,7 @@ class DomainRequest(TimeStampedModel):
"Domain",
null=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",
on_delete=models.SET_NULL,
)
@@ -612,7 +608,6 @@ class DomainRequest(TimeStampedModel):
"DraftDomain",
null=True,
blank=True,
- help_text="The requested domain",
related_name="domain_request",
on_delete=models.PROTECT,
)
@@ -621,6 +616,7 @@ class DomainRequest(TimeStampedModel):
"registrar.Website",
blank=True,
related_name="alternatives+",
+ help_text="Other domain names the creator provided for consideration",
)
# This is the contact information provided by the domain requestor. The
@@ -631,12 +627,12 @@ class DomainRequest(TimeStampedModel):
blank=True,
related_name="submitted_domain_requests",
on_delete=models.PROTECT,
+ help_text='Person listed under "your contact information" in the request form; will receive email updates',
)
purpose = models.TextField(
null=True,
blank=True,
- help_text="Purpose of your domain",
)
other_contacts = models.ManyToManyField(
@@ -649,13 +645,38 @@ class DomainRequest(TimeStampedModel):
no_other_contacts_rationale = models.TextField(
null=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(
null=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(
@@ -676,7 +697,6 @@ class DomainRequest(TimeStampedModel):
notes = models.TextField(
null=True,
blank=True,
- help_text="Notes about this request",
)
def sync_organization_type(self):
@@ -711,8 +731,33 @@ class DomainRequest(TimeStampedModel):
def save(self, *args, **kwargs):
"""Save override for custom properties"""
self.sync_organization_type()
+ self.sync_yes_no_form_fields()
+
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):
try:
if self.requested_domain and self.requested_domain.name:
@@ -1051,6 +1096,16 @@ class DomainRequest(TimeStampedModel):
"""Does this domain request have other contacts listed?"""
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]:
"""Is this domain request for a federal agency?
diff --git a/src/registrar/models/host.py b/src/registrar/models/host.py
index 2fb4b980b..97ee1f8aa 100644
--- a/src/registrar/models/host.py
+++ b/src/registrar/models/host.py
@@ -22,14 +22,13 @@ class Host(TimeStampedModel):
default=None, # prevent saving without a value
unique=False,
verbose_name="host name",
- help_text="Fully qualified domain name",
)
domain = models.ForeignKey(
"registrar.Domain",
on_delete=models.PROTECT,
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):
diff --git a/src/registrar/models/host_ip.py b/src/registrar/models/host_ip.py
index 216ad9eca..e0c5bb1c9 100644
--- a/src/registrar/models/host_ip.py
+++ b/src/registrar/models/host_ip.py
@@ -21,12 +21,11 @@ class HostIP(TimeStampedModel):
default=None, # prevent saving without a value
validators=[validate_ipv46_address],
verbose_name="IP address",
- help_text="IP address",
)
host = models.ForeignKey(
"registrar.Host",
on_delete=models.PROTECT,
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",
)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index cf027e70c..5e4c88f63 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -2,6 +2,7 @@ import logging
from django.contrib.auth.models import AbstractUser
from django.db import models
+from django.db.models import Q
from registrar.models.user_domain_role import UserDomainRole
@@ -23,6 +24,28 @@ class User(AbstractUser):
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 ####
RESTRICTED = "restricted"
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
@@ -34,6 +57,7 @@ class User(AbstractUser):
null=True, # Allow the field to be null
blank=True, # Allow the field to be blank
verbose_name="user status",
+ help_text='Users in "restricted" status cannot make updates in the registrar or start a new request.',
)
domains = models.ManyToManyField(
@@ -49,6 +73,13 @@ class User(AbstractUser):
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):
# this info is pulled from Login.gov
if self.first_name or self.last_name:
@@ -113,23 +144,61 @@ class User(AbstractUser):
except Exception as err:
raise err
- # 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
- # in the username field of a TransitionDomain)
- if TransitionDomain.objects.filter(username=email).exists():
- return False
+ # 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)
- # New users flagged by Staff to bypass ial2
- if VerifiedByStaff.objects.filter(email=email).exists():
- return False
+ # 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
- # 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").
- invited = DomainInvitation.DomainInvitationStatus.INVITED
- if DomainInvitation.objects.filter(email=email, status=invited).exists():
- return False
+ 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)
- return True
+ # 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
+ # that we inputted from Verisign (that is, their email address appears
+ # in the username field of a TransitionDomain)
+ verification_type = cls.VerificationTypeChoices.GRANDFATHERED
+ elif VerifiedByStaff.objects.filter(email=email).exists():
+ # New users flagged by Staff to bypass ial2
+ verification_type = cls.VerificationTypeChoices.VERIFIED_BY_STAFF
+ 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,
+ # their email address is in DomainInvitation for an invitation that is not yet "retrieved").
+ verification_type = cls.VerificationTypeChoices.INVITED
+ else:
+ verification_type = cls.VerificationTypeChoices.REGULAR
+
+ return verification_type
def check_domain_invitations_on_login(self):
"""When a user first arrives on the site, we need to retrieve any domain
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index 12ec85e57..d1d890da4 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -167,7 +167,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# 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.
# Or if there are pre-existing data.
- logger.warning(
+ logger.debug(
"create_or_update_organization_type() -> is_election_board "
f"cannot exist for {generic_org_type}. Setting to None."
)
diff --git a/src/registrar/models/verified_by_staff.py b/src/registrar/models/verified_by_staff.py
index a6d861504..c09dce822 100644
--- a/src/registrar/models/verified_by_staff.py
+++ b/src/registrar/models/verified_by_staff.py
@@ -9,7 +9,6 @@ class VerifiedByStaff(TimeStampedModel):
email = models.EmailField(
null=False,
blank=False,
- help_text="Email",
db_index=True,
)
@@ -19,12 +18,12 @@ class VerifiedByStaff(TimeStampedModel):
blank=True,
on_delete=models.SET_NULL,
related_name="verifiedby_user",
+ help_text="Person who verified this user",
)
notes = models.TextField(
null=False,
blank=False,
- help_text="Notes",
)
class Meta:
diff --git a/src/registrar/models/website.py b/src/registrar/models/website.py
index 29739b8ee..a062fe248 100644
--- a/src/registrar/models/website.py
+++ b/src/registrar/models/website.py
@@ -12,7 +12,7 @@ class Website(TimeStampedModel):
website = models.CharField(
max_length=255,
null=False,
- help_text="",
+ help_text="An alternative domain or current website listed on a domain request",
)
def __str__(self) -> str:
diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html
index 58843421a..dd680cec5 100644
--- a/src/registrar/templates/admin/base_site.html
+++ b/src/registrar/templates/admin/base_site.html
@@ -23,6 +23,7 @@
+
{% endblock %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
diff --git a/src/registrar/templates/admin/fieldset.html b/src/registrar/templates/admin/fieldset.html
index 8b8972e08..19c5db294 100644
--- a/src/registrar/templates/admin/fieldset.html
+++ b/src/registrar/templates/admin/fieldset.html
@@ -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
{% endcomment %}