merged with origin and resolved conflict

This commit is contained in:
Alysia Broddrick 2024-01-22 16:54:09 -08:00
commit 3dfc2a598e
No known key found for this signature in database
GPG key ID: 03917052CD0F06B7
98 changed files with 3599 additions and 1008 deletions

View file

@ -2,7 +2,7 @@
This diagram connects the data models along with various workflow stages. This diagram connects the data models along with various workflow stages.
1. The applicant starts the process at `/register` interacting with the 1. The applicant starts the process at `/request` interacting with the
`DomainApplication` object. `DomainApplication` object.
2. The analyst approves the application using the `DomainApplication`'s 2. The analyst approves the application using the `DomainApplication`'s
@ -139,7 +139,7 @@ DomainInvitation -- Domain
DomainInvitation .[#green].> UserDomainRole : User.on_each_login() DomainInvitation .[#green].> UserDomainRole : User.on_each_login()
actor applicant #Red actor applicant #Red
applicant -d-> DomainApplication : **/register** applicant -d-> DomainApplication : **/request**
actor analyst #Blue actor analyst #Blue
analyst -[#blue]-> DomainApplication : **approve()** analyst -[#blue]-> DomainApplication : **approve()**

View file

@ -21,28 +21,6 @@
- Notes: Subject line of the "Domain Request Withdrawn" email - Notes: Subject line of the "Domain Request Withdrawn" email
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn_subject.txt) - [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn_subject.txt)
## Status Change Action Needed
- Starting Location: Django Admin
- Workflow: Analyst Admin
- Workflow Step: Click "Domain applications" -> Click an application with a status of "in review" or "rejected" -> Click status dropdown -> (select "action needed") -> click "Save"
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, set the status to either "in review" or "rejected" (and click save), then set the status to "action needed". This will send you an email.
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_action_needed.txt)
### Status Change Action Needed Subject
- Notes: Subject line of the "Status Change Action Needed" email
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_action_needed_subject.txt)
## Status Change in Review
- Starting Location: Django Admin
- Workflow: Analyst Admin
- Workflow Step: Click "Domain applications" -> Click an application with a status of "submitted" -> Click status dropdown -> (select "In review") -> click "Save"
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, then set the status to "In review". This will send you an email.
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_approved.txt)
### Status Change in Review Subject
- Notes: This is the subject line of the "Status Change In Review" email
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_in_review_subject.txt)
## Status Change Approved ## Status Change Approved
- Starting Location: Django Admin - Starting Location: Django Admin
- Workflow: Analyst Admin - Workflow: Analyst Admin

View file

@ -41,11 +41,11 @@ This happens when you swap branches on your sandbox that contain diverging leave
- `cf login -a api.fr.cloud.gov --sso` - `cf login -a api.fr.cloud.gov --sso`
- `cf ssh getgov-<app>` - `cf ssh getgov-<app>`
- `/tmp/lifecycle/shell` - `/tmp/lifecycle/shell`
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 39_previous_miration --fake' --name migrate` - Find the conflicting migrations: `./manage.py showmigrations`
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 41_example_migration' --name migrate` - Delete one of them: `rm registrar/migrations/0041_example.py`
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 45_last_migration --fake' --name migrate` - `/manage.py showmigrations`
- `/manage.py makemigrations`
Then, navigate to and delete the offending migration. In this case, it is 0041_example_migration. - `/manage.py migrate`
### Scenario 3: Migrations ran incorrectly, and migrate no longer works (sandbox) ### Scenario 3: Migrations ran incorrectly, and migrate no longer works (sandbox)
@ -63,7 +63,7 @@ To diagnose this issue, you will have to manually delete tables using the psql s
1. `cf login -a api.fr.cloud.gov --sso` 1. `cf login -a api.fr.cloud.gov --sso`
2. Run `cf connect-to-service -no-client getgov-{environment_name} getgov-{environment_name}-database` to open a SSH tunnel 2. Run `cf connect-to-service -no-client getgov-{environment_name} getgov-{environment_name}-database` to open a SSH tunnel
3. Run `psql -h localhost -p {port} -U {username} -d {broker_name}` 3. Run `psql -h localhost -p {port} -U {username} -d {broker_name}`
4. Open a new terminal window and run `cf ssh getgov{environment_name}` 4. Open a new terminal window and run `cf ssh getgov-{environment_name}`
5. Within that window, run `tmp/lifecycle/shell` 5. Within that window, run `tmp/lifecycle/shell`
6. Within that window, run `./manage.py migrate` and observe which tables are duplicates 6. Within that window, run `./manage.py migrate` and observe which tables are duplicates
@ -102,7 +102,7 @@ Example: there are extra columns created on a table by an old migration long sin
Example: You are able to log in and access the /admin page, but when you arrive at the registrar you keep getting 500 errors and your log-ins any API calls you make via the UI does not show up in the log stream. And you feel like youre starting to lose your marbles. Example: You are able to log in and access the /admin page, but when you arrive at the registrar you keep getting 500 errors and your log-ins any API calls you make via the UI does not show up in the log stream. And you feel like youre starting to lose your marbles.
In the CLI, run the command `cf routes` In the CLI, run the command `cf routes`
If you notice that your route of `getgov-<app>.app.cloud.gov` is pointing two apps, then that is probably the major issue of the 500 error. (ie mine was pointing at `getgov-<app>.app.cloud.gov` AND `cisa-dotgov` If you notice that your route of `getgov-<app>.app.cloud.gov` is pointing two apps, then that is probably the major issue of the 500 error. (ie mine was pointing at `getgov-<app>.app.cloud.gov` AND `cisa-dotgov`)
In the CLI, run the command `cf apps` to check that it has an app running called `cisa-dotgov`. If so, theres the error! In the CLI, run the command `cf apps` to check that it has an app running called `cisa-dotgov`. If so, theres the error!
Essentially this shows that your requests were being handled by two completely separate applications and thats why some requests arent being located. Essentially this shows that your requests were being handled by two completely separate applications and thats why some requests arent being located.
To resolve this issue, remove the app named `cisa-dotgov` from this space. To resolve this issue, remove the app named `cisa-dotgov` from this space.
@ -117,7 +117,7 @@ https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1697810600723069
### Scenario 8: Cant log into sandbox, permissions do not exist ### Scenario 8: Cant log into sandbox, permissions do not exist
- Fake migrate the migration thats before the last data creation migration 1. `./manage.py migrate --fake model_name_here file_name_BEFORE_the_most_recent_CREATE_migration` (fake migrate the migration thats before the last data creation migration -- look for number_create, and then copy the file BEFORE it)
- Run the last data creation migration (AND ONLY THAT ONE) 2. `./manage.py migrate model_name_here file_name_WITH_create` (run the last data creation migration AND ONLY THAT ONE)
- Fake migrate the last migration in the migration list 3. `./manage.py migrate --fake model_name_here most_recent_file_name` (fake migrate the last migration in the migration list)
- Rerun fixtures 4. `./manage.py load` (rerun fixtures)

View file

@ -35,17 +35,55 @@ Binding the database in `manifest-<ENVIRONMENT>.json` automatically inserts the
# Deploy # Deploy
We have three types of environments: developer "sandboxes", `staging` and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `staging` and `stable`, and it does so when we make tagged releases of `main`. For `staging`, this is done to ensure there is a non-production level test envirornment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`. We have four types of environments: developer "sandboxes", `development`, `staging` and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `development`, `staging` and `stable`.
You should make sure all of the USWDS assets are compiled and collected before deploying to your sandbox. To deploy locally to `sandbox`: For staging and stable our CD service completes this deploy when we make tagged releases from specifc branch. For `staging`, this is done to ensure there is a non-production level test environment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. When deploying to staging, the branch used is often just `main`. On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`. Thus the branch used in `stable` releases is usually the tagged branch used for the last staging commit.
The `development` environment, is one that auto deploys on any push to main via our CD service. This is to ensure we have an environment that is identical to what we have on the `main` branch. This should not be confused with the "sandboxes" given to developers and designers for ticket development.
When deploying to your personal sandbox, you should make sure all of the USWDS assets are compiled and collected before deploying to your sandbox. To deploy locally to `sandbox`:
For ease of use, you can run the `deploy.sh <sandbox name>` script in the `/src` directory to build the assets and deploy to your sandbox. Similarly, you could run `build.sh <sandbox name>` script to just compile and collect the assets without deploying. For ease of use, you can run the `deploy.sh <sandbox name>` script in the `/src` directory to build the assets and deploy to your sandbox. Similarly, you could run `build.sh <sandbox name>` script to just compile and collect the assets without deploying.
Your sandbox space should've been setup as part of the onboarding process. If this was not the case, please have an admin follow the instructions [here](../../.github/ISSUE_TEMPLATE/developer-onboarding.md#setting-up-developer-sandbox). Your sandbox space should've been setup as part of the onboarding process. If this was not the case, please have an admin follow the instructions below.
## Creating a sandbox or new environment
When possible all developers and designers should have their own sandboxes as this provides them a space to test out changes in an isolated environment. All sandboxes are still accessible on the web, just like `staging`, `stable`, and `development`.
1. Make sure you have admin access to the cloud.gov organization, have admin access on github, and make sure you are targeting your own workspace in cloudfoundry
2. Make sure you are on `main` and your local code is up to date with the repo
3. Open the terminal to the root project directory
4. run [creating a developer sandbox shell script](../../ops/scripts/create_dev_sandbox.sh) by typing the path to the script followed by the name of the sandbox you wish to create. Use initials for the sandbox name. If John Doe is the name of a developer you wish to make a sandbox for you would then do:
```
./ops/scripts/create_dev_sandbox.sh jd
```
5. Follow the prompts that appear in the terminal, if on `main`, make sure to click yes to switching to a new branch. Clicking anything besides `Y` or `y` will count as a no.
6. When the database is being set up it can take 5 mins or longer, don't close the window.
7. The last prompt asks if you want to make a PR, this will generate a PR for you but you may need to double check against similiar PRs to make sure everything was changed correctly. To do this go to github Pull Requests and search for closed PRs with the word infrastructure.
## Once the sandbox or new environment is made
Once this is made, the new owner of the sandbox has a few steps they should follow. This is already in [onboarding documents](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.6dw0iz1u56ox), but is worth re-iterating here:
1. Run fixtures if desired. Refer to the [onboarding guide](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.6dw0iz1u56ox) for how to do this and helpful hints
2. add environment variables for registrar-registry communication (EPP), see [the application secrets readme](./runbooks/rotate_application_secrets.md)
## Creating a new environment
If we ever need a new environment to replace `development`, `staging` or `stable` we need to follow similiar steps but not identical ones to the instructions for making a sandbox.
1. Just like making a sandbox make sure you have admin access to the cloud.gov organization, have admin access on github, and make sure you are targeting your own workspace in cloudfoundry. Make sure you are on `main` and your local code is up to date with the repo
2. Open the terminal to the root project directory.
3. Instead of running [the script for creating a sandbox](../../ops/scripts/create_dev_sandbox.sh), you will manually copy over which commands you want directly into the terminal. Don't run the prompts in terminal, as you will be figuring out what you need to do based on your needs. All the prompts, denoted with `echo`, tell you what the following commands are doing. When uncertain look at the cloudfoundry documentation for any of the `cf` commands.
4. In most cases, the setup will be almost identical to making a sandbox. The main difference will be deployment and determining if you want workflows like reset, deploy, and migrate to work for it. You will manually update these yaml files if you want the workflows included.
5. Often it is the manifest file that needs to change as well, either with different environment variables, number of instances, or so on. Copy whichever manifest is closest to what you wish to do and tailor it to your specific needs. See cloudfoundry's and docker's documentation if you need assistance.
## Stable and Staging Release Rules ## Stable and Staging Release Rules
Releases will be made for staging and stable every week starting on the first day of the sprint (Wednesday), with the second release of the sprint occuring halfway through the sprint. With the exception of first time going into production, these releases will NOT have the same code. The release to stable will be the same commit that was tagged for staging one week prior, making stable one week behind staging. Further, this means staging can be up to a week behind the main branch of code. Releases will be made for staging and stable twice a week, ideally Tuesday and Thursday, but can be adjusted if needed. Code on `main` will be released to `staging`, and then on the following Tuesday/Thursday this `staging` release will become the new `stable` release. This means every release day, a release will be made to `stable` containing the last `staging` code. On this same day a new `staging` release will be made that contains the most up-to-date code on main. Thus, `staging` can be a few days behind the main branch, and `stable` will be a few days behind the code on `staging`.
If a bug fix or feature needs to be made to stable out of the normal cycle, this can only be done at the product owner's request. If a bug fix or feature needs to be made to stable out of the normal cycle, this can only be done at the product owner's request.

View file

@ -524,3 +524,65 @@ Example: `cf ssh getgov-za`
| 2 | **debug** | Increases logging detail. Defaults to False. | | 2 | **debug** | Increases logging detail. Defaults to False. |
| 3 | **limitParse** | Determines how many domains to parse. Defaults to all. | | 3 | **limitParse** | Determines how many domains to parse. Defaults to all. |
| 4 | **disableIdempotentCheck** | Boolean that determines if we should check for idempotence or not. Compares the proposed extension date to the value in TransitionDomains. Defaults to False. | | 4 | **disableIdempotentCheck** | Boolean that determines if we should check for idempotence or not. Compares the proposed extension date to the value in TransitionDomains. Defaults to False. |
## Populate First Ready
This section outlines how to run the populate_first_ready script
### 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_first_ready --debug```
### Running locally
```docker-compose exec app ./manage.py populate_first_ready --debug```
##### Optional parameters
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **debug** | Increases logging detail. Defaults to False. |
## Patch Federal Agency Info
This section outlines how to use `patch_federal_agency_info.py`
### Running on sandboxes
#### Step 1: Grab the latest `current-full.csv` file from the dotgov-data repo
Download the csv from [here](https://github.com/cisagov/dotgov-data/blob/main/current-full.csv) and place this file under the `src/migrationdata/` directory.
#### Step 2: Transfer the `current-full.csv` file to your sandbox
[Click here to go to the section about transferring data to sandboxes](#step-1-transfer-data-to-sandboxes)
#### Step 3: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 4: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 5: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 6: Patch agency info
```./manage.py patch_federal_agency_info migrationdata/current-full.csv --debug```
### Running locally
```docker-compose exec app ./manage.py patch_federal_agency_info migrationdata/current-full.csv --debug```
##### Optional parameters
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **debug** | Increases logging detail. Defaults to False. |

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Which OIDC provider to use # Which OIDC provider to use
OIDC_ACTIVE_PROVIDER: login.gov production OIDC_ACTIVE_PROVIDER: login.gov production
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# use a non-default route to avoid conflicts # use a non-default route to avoid conflicts
routes: routes:
- route: getgov-ENVIRONMENT-migrate.app.cloud.gov - route: getgov-ENVIRONMENT-migrate.app.cloud.gov

View file

@ -22,7 +22,7 @@ applications:
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: INFO
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments
IS_PRODUCTION: False IS_PRODUCTION: False
routes: routes:

View file

@ -6,19 +6,19 @@
"urls": [ "urls": [
"http://localhost:8080/", "http://localhost:8080/",
"http://localhost:8080/health/", "http://localhost:8080/health/",
"http://localhost:8080/register/", "http://localhost:8080/request/",
"http://localhost:8080/register/organization/", "http://localhost:8080/request/organization/",
"http://localhost:8080/register/org_federal/", "http://localhost:8080/request/org_federal/",
"http://localhost:8080/register/org_election/", "http://localhost:8080/request/org_election/",
"http://localhost:8080/register/org_contact/", "http://localhost:8080/request/org_contact/",
"http://localhost:8080/register/authorizing_official/", "http://localhost:8080/request/authorizing_official/",
"http://localhost:8080/register/current_sites/", "http://localhost:8080/request/current_sites/",
"http://localhost:8080/register/dotgov_domain/", "http://localhost:8080/request/dotgov_domain/",
"http://localhost:8080/register/purpose/", "http://localhost:8080/request/purpose/",
"http://localhost:8080/register/your_contact/", "http://localhost:8080/request/your_contact/",
"http://localhost:8080/register/other_contacts/", "http://localhost:8080/request/other_contacts/",
"http://localhost:8080/register/anything_else/", "http://localhost:8080/request/anything_else/",
"http://localhost:8080/register/requirements/", "http://localhost:8080/request/requirements/",
"http://localhost:8080/register/finished/" "http://localhost:8080/request/finished/"
] ]
} }

View file

@ -1,10 +1,11 @@
"""Internal API views""" """Internal API views"""
from django.apps import apps from django.apps import apps
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from registrar.templatetags.url_helpers import public_site_url from registrar.templatetags.url_helpers import public_site_url
from registrar.utility.enums import ValidationReturnType
from registrar.utility.errors import GenericError, GenericErrorCodes from registrar.utility.errors import GenericError, GenericErrorCodes
import requests import requests
@ -71,6 +72,7 @@ def check_domain_available(domain):
a match. If check fails, throws a RegistryError. a match. If check fails, throws a RegistryError.
""" """
Domain = apps.get_model("registrar.Domain") Domain = apps.get_model("registrar.Domain")
if domain.endswith(".gov"): if domain.endswith(".gov"):
return Domain.available(domain) return Domain.available(domain)
else: else:
@ -86,22 +88,14 @@ def available(request, domain=""):
Response is a JSON dictionary with the key "available" and value true or Response is a JSON dictionary with the key "available" and value true or
false. false.
""" """
Domain = apps.get_model("registrar.Domain")
domain = request.GET.get("domain", "") domain = request.GET.get("domain", "")
DraftDomain = apps.get_model("registrar.DraftDomain")
# validate that the given domain could be a domain name and fail early if _, json_response = Domain.validate_and_handle_errors(
# not. domain=domain,
if not (DraftDomain.string_could_be_domain(domain) or DraftDomain.string_could_be_domain(domain + ".gov")): return_type=ValidationReturnType.JSON_RESPONSE,
return JsonResponse({"available": False, "code": "invalid", "message": DOMAIN_API_MESSAGES["invalid"]}) )
# a domain is available if it is NOT in the list of current domains return json_response
try:
if check_domain_available(domain):
return JsonResponse({"available": True, "code": "success", "message": DOMAIN_API_MESSAGES["success"]})
else:
return JsonResponse(
{"available": False, "code": "unavailable", "message": DOMAIN_API_MESSAGES["unavailable"]}
)
except Exception:
return JsonResponse({"available": False, "code": "error", "message": DOMAIN_API_MESSAGES["error"]})
@require_http_methods(["GET"]) @require_http_methods(["GET"])

View file

@ -32,7 +32,7 @@ services:
# Is this a production environment # Is this a production environment
- IS_PRODUCTION - IS_PRODUCTION
# Public site URL link # Public site URL link
- GETGOV_PUBLIC_SITE_URL=https://beta.get.gov - GETGOV_PUBLIC_SITE_URL=https://get.gov
# Set a username for accessing the registry # Set a username for accessing the registry
- REGISTRY_CL_ID=nothing - REGISTRY_CL_ID=nothing
# Set a password for accessing the registry # Set a password for accessing the registry

View file

@ -401,6 +401,8 @@ class HostIPInline(admin.StackedInline):
class MyHostAdmin(AuditedAdmin): class MyHostAdmin(AuditedAdmin):
"""Custom host admin class to use our inlines.""" """Custom host admin class to use our inlines."""
search_fields = ["name", "domain__name"]
search_help_text = "Search by domain or hostname."
inlines = [HostIPInline] inlines = [HostIPInline]
@ -1252,7 +1254,7 @@ admin.site.register(models.Domain, DomainAdmin)
admin.site.register(models.DraftDomain, DraftDomainAdmin) admin.site.register(models.DraftDomain, DraftDomainAdmin)
# Host and HostIP removed from django admin because changes in admin # Host and HostIP removed from django admin because changes in admin
# do not propagate to registry and logic not applied # do not propagate to registry and logic not applied
# admin.site.register(models.Host, MyHostAdmin) admin.site.register(models.Host, MyHostAdmin)
admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.Website, WebsiteAdmin)
admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.PublicContact, AuditedAdmin)
admin.site.register(models.DomainApplication, DomainApplicationAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin)

View file

@ -134,10 +134,19 @@ function _checkDomainAvailability(el) {
const callback = (response) => { const callback = (response) => {
toggleInputValidity(el, (response && response.available), msg=response.message); toggleInputValidity(el, (response && response.available), msg=response.message);
announce(el.id, response.message); announce(el.id, response.message);
// Determines if we ignore the field if it is just blank
ignore_blank = el.classList.contains("blank-ok")
if (el.validity.valid) { if (el.validity.valid) {
el.classList.add('usa-input--success'); el.classList.add('usa-input--success');
// use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration
inlineToast(el.parentElement, el.id, SUCCESS, response.message); inlineToast(el.parentElement, el.id, SUCCESS, response.message);
} else if (ignore_blank && response.code == "required"){
// Visually remove the error
error = "usa-input--error"
if (el.classList.contains(error)){
el.classList.remove(error)
}
} else { } else {
inlineToast(el.parentElement, el.id, ERROR, response.message); inlineToast(el.parentElement, el.id, ERROR, response.message);
} }
@ -229,99 +238,210 @@ function handleValidationClick(e) {
} }
})(); })();
/**
* Delete method for formsets that diff in the view and delete in the model (Nameservers, DS Data)
*
*/
function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
let formToRemove = e.target.closest(".repeatable-form");
formToRemove.remove();
let forms = document.querySelectorAll(".repeatable-form");
totalForms.setAttribute('value', `${forms.length}`);
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g');
// For the example on Nameservers
let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g');
forms.forEach((form, index) => {
// Iterate over child nodes of the current element
Array.from(form.querySelectorAll('label, input, select')).forEach((node) => {
// Iterate through the attributes of the current node
Array.from(node.attributes).forEach((attr) => {
// Check if the attribute value matches the regex
if (formNumberRegex.test(attr.value)) {
// Replace the attribute value with the updated value
attr.value = attr.value.replace(formNumberRegex, `form-${index}-`);
}
});
});
// h2 and legend for DS form, label for nameservers
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
let innerSpan = node.querySelector('span')
if (innerSpan) {
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
} else {
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
}
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
// inject the USWDS required markup and make sure the INPUT is required
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
// Remove the word optional
innerSpan.textContent = innerSpan.textContent.replace(/\s*\(\s*optional\s*\)\s*/, '');
// Create a new element
const newElement = document.createElement('abbr');
newElement.textContent = '*';
newElement.setAttribute("title", "required");
newElement.classList.add("usa-hint", "usa-hint--required");
// Append the new element to the label
node.appendChild(newElement);
// Find the next sibling that is an input element
let nextInputElement = node.nextElementSibling;
while (nextInputElement) {
if (nextInputElement.tagName === 'INPUT') {
// Found the next input element
nextInputElement.setAttribute("required", "")
break;
}
nextInputElement = nextInputElement.nextElementSibling;
}
nextInputElement.required = true;
}
});
// Display the add more button if we have less than 13 forms
if (isNameserversForm && forms.length <= 13) {
addButton.removeAttribute("disabled");
}
if (isNameserversForm && forms.length < 3) {
// Hide the delete buttons on the remaining nameservers
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
}
});
}
/** /**
* Prepare the namerservers and DS data forms delete buttons * Delete method for formsets using the DJANGO DELETE widget (Other Contacts)
* We will call this on the forms init, and also every time we add a form *
*/
function markForm(e, formLabel){
// Unlike removeForm, we only work with the visible forms when using DJANGO's DELETE widget
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
if (totalShownForms == 1) {
// toggle the radio buttons
let radioButton = document.querySelector('input[name="other_contacts-has_other_contacts"][value="False"]');
radioButton.checked = true;
// Trigger the change event
let event = new Event('change');
radioButton.dispatchEvent(event);
} else {
// Grab the hidden delete input and assign a value DJANGO will look for
let formToRemove = e.target.closest(".repeatable-form");
if (formToRemove) {
let deleteInput = formToRemove.querySelector('input[class="deletion"]');
if (deleteInput) {
deleteInput.value = 'on';
}
}
// Set display to 'none'
formToRemove.style.display = 'none';
}
// Update h2s on the visible forms only. We won't worry about the forms' identifiers
let shownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`);
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g');
shownForms.forEach((form, index) => {
// Iterate over child nodes of the current element
Array.from(form.querySelectorAll('h2')).forEach((node) => {
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
});
});
}
/**
* Prepare the namerservers, DS data and Other Contacts formsets' delete button
* for the last added form. We call this from the Add function
*
*/
function prepareNewDeleteButton(btn, formLabel) {
let formIdentifier = "form"
let isNameserversForm = document.querySelector(".nameservers-form");
let isOtherContactsForm = document.querySelector(".other-contacts-form");
let addButton = document.querySelector("#add-form");
if (isOtherContactsForm) {
formIdentifier = "other_contacts";
// We will mark the forms for deletion
btn.addEventListener('click', function(e) {
markForm(e, formLabel);
});
} else {
// We will remove the forms and re-order the formset
btn.addEventListener('click', function(e) {
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
});
}
}
/**
* Prepare the namerservers, DS data and Other Contacts formsets' delete buttons
* We will call this on the forms init
* *
*/ */
function prepareDeleteButtons(formLabel) { function prepareDeleteButtons(formLabel) {
let formIdentifier = "form"
let deleteButtons = document.querySelectorAll(".delete-record"); let deleteButtons = document.querySelectorAll(".delete-record");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); let isNameserversForm = document.querySelector(".nameservers-form");
let isNameserversForm = document.title.includes("DNS name servers |"); let isOtherContactsForm = document.querySelector(".other-contacts-form");
let addButton = document.querySelector("#add-form"); let addButton = document.querySelector("#add-form");
if (isOtherContactsForm) {
formIdentifier = "other_contacts";
}
// Loop through each delete button and attach the click event listener // Loop through each delete button and attach the click event listener
deleteButtons.forEach((deleteButton) => { deleteButtons.forEach((deleteButton) => {
deleteButton.addEventListener('click', removeForm); if (isOtherContactsForm) {
// We will mark the forms for deletion
deleteButton.addEventListener('click', function(e) {
markForm(e, formLabel);
});
} else {
// We will remove the forms and re-order the formset
deleteButton.addEventListener('click', function(e) {
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
});
}
}); });
}
function removeForm(e){ /**
let formToRemove = e.target.closest(".repeatable-form"); * DJANGO formset's DELETE widget
formToRemove.remove(); * On form load, hide deleted forms, ie. those forms with hidden input of class 'deletion'
let forms = document.querySelectorAll(".repeatable-form"); * with value='on'
totalForms.setAttribute('value', `${forms.length}`); */
function hideDeletedForms() {
let hiddenDeleteButtonsWithValueOn = document.querySelectorAll('input[type="hidden"].deletion[value="on"]');
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g'); // Iterating over the NodeList of hidden inputs
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g'); hiddenDeleteButtonsWithValueOn.forEach(function(hiddenInput) {
// For the example on Nameservers // Finding the closest parent element with class "repeatable-form" for each hidden input
let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g'); var repeatableFormToHide = hiddenInput.closest('.repeatable-form');
forms.forEach((form, index) => { // Checking if a matching parent element is found for each hidden input
// Iterate over child nodes of the current element if (repeatableFormToHide) {
Array.from(form.querySelectorAll('label, input, select')).forEach((node) => { // Setting the display property to "none" for each matching parent element
// Iterate through the attributes of the current node repeatableFormToHide.style.display = 'none';
Array.from(node.attributes).forEach((attr) => {
// Check if the attribute value matches the regex
if (formNumberRegex.test(attr.value)) {
// Replace the attribute value with the updated value
attr.value = attr.value.replace(formNumberRegex, `form-${index}-`);
}
});
});
// h2 and legend for DS form, label for nameservers
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
// inject the USWDS required markup and make sure the INPUT is required
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
// Create a new element
const newElement = document.createElement('abbr');
newElement.textContent = '*';
newElement.setAttribute("title", "required");
newElement.classList.add("usa-hint", "usa-hint--required");
// Append the new element to the label
node.appendChild(newElement);
// Find the next sibling that is an input element
let nextInputElement = node.nextElementSibling;
while (nextInputElement) {
if (nextInputElement.tagName === 'INPUT') {
// Found the next input element
nextInputElement.setAttribute("required", "")
break;
}
nextInputElement = nextInputElement.nextElementSibling;
}
nextInputElement.required = true;
}
let innerSpan = node.querySelector('span')
if (innerSpan) {
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
} else {
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
}
});
// Display the add more button if we have less than 13 forms
if (isNameserversForm && forms.length <= 13) {
console.log('remove disabled');
addButton.removeAttribute("disabled");
} }
});
if (isNameserversForm && forms.length < 3) {
// Hide the delete buttons on the remaining nameservers
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
}
});
}
} }
/** /**
@ -331,25 +451,38 @@ function prepareDeleteButtons(formLabel) {
* it everywhere. * it everywhere.
*/ */
(function prepareFormsetsForms() { (function prepareFormsetsForms() {
let formIdentifier = "form"
let repeatableForm = document.querySelectorAll(".repeatable-form"); let repeatableForm = document.querySelectorAll(".repeatable-form");
let container = document.querySelector("#form-container"); let container = document.querySelector("#form-container");
let addButton = document.querySelector("#add-form"); let addButton = document.querySelector("#add-form");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
let cloneIndex = 0; let cloneIndex = 0;
let formLabel = ''; let formLabel = '';
let isNameserversForm = document.title.includes("DNS name servers |"); let isNameserversForm = document.querySelector(".nameservers-form");
let isOtherContactsForm = document.querySelector(".other-contacts-form");
let isDsDataForm = document.querySelector(".ds-data-form");
// The Nameservers formset features 2 required and 11 optionals
if (isNameserversForm) { if (isNameserversForm) {
cloneIndex = 2; cloneIndex = 2;
formLabel = "Name server"; formLabel = "Name server";
} else if ((document.title.includes("DS Data |")) || (document.title.includes("Key Data |"))) { // DNSSEC: DS Data
formLabel = "DS Data record"; } else if (isDsDataForm) {
formLabel = "DS data record";
// The Other Contacts form
} else if (isOtherContactsForm) {
formLabel = "Organization contact";
container = document.querySelector("#other-employees");
formIdentifier = "other_contacts"
} }
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
// On load: Disable the add more button if we have 13 forms // On load: Disable the add more button if we have 13 forms
if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) { if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) {
addButton.setAttribute("disabled", "true"); addButton.setAttribute("disabled", "true");
} }
// Hide forms which have previously been deleted
hideDeletedForms()
// Attach click event listener on the delete buttons of the existing forms // Attach click event listener on the delete buttons of the existing forms
prepareDeleteButtons(formLabel); prepareDeleteButtons(formLabel);
@ -360,7 +493,7 @@ function prepareDeleteButtons(formLabel) {
let forms = document.querySelectorAll(".repeatable-form"); let forms = document.querySelectorAll(".repeatable-form");
let formNum = forms.length; let formNum = forms.length;
let newForm = repeatableForm[cloneIndex].cloneNode(true); let newForm = repeatableForm[cloneIndex].cloneNode(true);
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g'); let formNumberRegex = RegExp(`${formIdentifier}-(\\d){1}-`,'g');
let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g'); let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g');
// For the eample on Nameservers // For the eample on Nameservers
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g'); let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
@ -393,16 +526,35 @@ function prepareDeleteButtons(formLabel) {
} }
formNum++; formNum++;
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`); newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`);
if (isOtherContactsForm) {
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
// since the form on the backend employs Django's DELETE widget.
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`);
} else {
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
// if indices 0 or 1 have been deleted
let containsOptional = newForm.innerHTML.includes('(optional)');
if (isNameserversForm && !containsOptional) {
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum} (optional)`);
} else {
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
}
}
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`); newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
newForm.innerHTML = newForm.innerHTML.replace(/\n/g, ''); // Remove newline characters
newForm.innerHTML = newForm.innerHTML.replace(/>\s*</g, '><'); // Remove spaces between tags
container.insertBefore(newForm, addButton); container.insertBefore(newForm, addButton);
newForm.style.display = 'block';
let inputs = newForm.querySelectorAll("input"); let inputs = newForm.querySelectorAll("input");
// Reset the values of each input to blank // Reset the values of each input to blank
inputs.forEach((input) => { inputs.forEach((input) => {
input.classList.remove("usa-input--error"); input.classList.remove("usa-input--error");
if (input.type === "text" || input.type === "number" || input.type === "password") { if (input.type === "text" || input.type === "number" || input.type === "password" || input.type === "email" || input.type === "tel") {
input.value = ""; // Set the value to an empty string input.value = ""; // Set the value to an empty string
} else if (input.type === "checkbox" || input.type === "radio") { } else if (input.type === "checkbox" || input.type === "radio") {
@ -439,7 +591,8 @@ function prepareDeleteButtons(formLabel) {
totalForms.setAttribute('value', `${formNum}`); totalForms.setAttribute('value', `${formNum}`);
// Attach click event listener on the delete buttons of the new form // Attach click event listener on the delete buttons of the new form
prepareDeleteButtons(formLabel); let newDeleteButton = newForm.querySelector(".delete-record");
prepareNewDeleteButton(newDeleteButton, formLabel);
// Disable the add more button if we have 13 forms // Disable the add more button if we have 13 forms
if (isNameserversForm && formNum == 13) { if (isNameserversForm && formNum == 13) {
@ -483,3 +636,58 @@ function prepareDeleteButtons(formLabel) {
}, 50); }, 50);
} }
})(); })();
// 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"]');
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;
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);
}
}
if (radioButtons.length) {
// Add event listener to each radio button
radioButtons.forEach(function (radioButton) {
radioButton.addEventListener('change', handleRadioButtonChange);
});
// initialize
handleRadioButtonChange();
}
})();

View file

@ -4,6 +4,10 @@
.sr-only { .sr-only {
@include sr-only; @include sr-only;
} }
.clear-both {
clear: both;
}
* { * {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

View file

@ -31,3 +31,10 @@
padding-left: 0; padding-left: 0;
border-left: none; border-left: none;
} }
legend.float-left-tablet + button.float-right-tablet {
margin-top: .5rem;
@include at-media('tablet') {
margin-top: 1rem;
}
}

View file

@ -22,3 +22,9 @@ h2 {
margin: units(4) 0 units(1); margin: units(4) 0 units(1);
color: color('primary-darker'); color: color('primary-darker');
} }
// Normalize typography in forms
.usa-form,
.usa-form fieldset {
font-size: 1rem;
}

View file

@ -335,7 +335,7 @@ CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
# Cross-Origin Resource Sharing (CORS) configuration # Cross-Origin Resource Sharing (CORS) configuration
# Sets clients that allow access control to manage.get.gov # Sets clients that allow access control to manage.get.gov
# TODO: remove :8080 to see if we can have all localhost access # TODO: remove :8080 to see if we can have all localhost access
CORS_ALLOWED_ORIGINS = ["http://localhost:8080", "https://beta.get.gov"] CORS_ALLOWED_ORIGINS = ["http://localhost:8080", "https://beta.get.gov", "https://get.gov"]
CORS_ALLOWED_ORIGIN_REGEXES = [r"https://[\w-]+\.sites\.pages\.cloud\.gov"] CORS_ALLOWED_ORIGIN_REGEXES = [r"https://[\w-]+\.sites\.pages\.cloud\.gov"]
# Content-Length header is set by django.middleware.common.CommonMiddleware # Content-Length header is set by django.middleware.common.CommonMiddleware

View file

@ -39,7 +39,6 @@ for step, view in [
(Step.PURPOSE, views.Purpose), (Step.PURPOSE, views.Purpose),
(Step.YOUR_CONTACT, views.YourContact), (Step.YOUR_CONTACT, views.YourContact),
(Step.OTHER_CONTACTS, views.OtherContacts), (Step.OTHER_CONTACTS, views.OtherContacts),
(Step.NO_OTHER_CONTACTS, views.NoOtherContacts),
(Step.ANYTHING_ELSE, views.AnythingElse), (Step.ANYTHING_ELSE, views.AnythingElse),
(Step.REQUIREMENTS, views.Requirements), (Step.REQUIREMENTS, views.Requirements),
(Step.REVIEW, views.Review), (Step.REVIEW, views.Review),
@ -77,7 +76,7 @@ urlpatterns = [
), ),
path("health/", views.health), path("health/", views.health),
path("openid/", include("djangooidc.urls")), path("openid/", include("djangooidc.urls")),
path("register/", include((application_urls, APPLICATION_NAMESPACE))), path("request/", include((application_urls, APPLICATION_NAMESPACE))),
path("api/v1/available/", available, name="available"), path("api/v1/available/", available, name="available"),
path("api/v1/get-report/current-federal", get_current_federal, name="get-current-federal"), path("api/v1/get-report/current-federal", get_current_federal, name="get-current-federal"),
path("api/v1/get-report/current-full", get_current_full, name="get-current-full"), path("api/v1/get-report/current-full", get_current_full, name="get-current-full"),

View file

@ -2,17 +2,17 @@ from __future__ import annotations # allows forward references in annotations
from itertools import zip_longest from itertools import zip_longest
import logging import logging
from typing import Callable from typing import Callable
from api.views import DOMAIN_API_MESSAGES
from phonenumber_field.formfields import PhoneNumberField # type: ignore from phonenumber_field.formfields import PhoneNumberField # type: ignore
from django import forms from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator from django.core.validators import RegexValidator, MaxLengthValidator
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.db.models.fields.related import ForeignObjectRel
from api.views import DOMAIN_API_MESSAGES
from registrar.models import Contact, DomainApplication, DraftDomain, Domain from registrar.models import Contact, DomainApplication, DraftDomain, Domain
from registrar.templatetags.url_helpers import public_site_url from registrar.templatetags.url_helpers import public_site_url
from registrar.utility import errors from registrar.utility.enums import ValidationReturnType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -115,26 +115,45 @@ class RegistrarFormSet(forms.BaseFormSet):
query = getattr(obj, join).order_by("created_at").all() # order matters 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 # the use of `zip` pairs the forms in the formset with the
# related objects gotten from the database -- there should always be # related objects gotten from the database -- there should always be
# at least as many forms as database entries: extra forms means new # at least as many forms as database entries: extra forms means new
# entries, but fewer forms is _not_ the correct way to delete items # entries, but fewer forms is _not_ the correct way to delete items
# (likely a client-side error or an attempt at data tampering) # (likely a client-side error or an attempt at data tampering)
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None): 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 {} cleaned = post_data.cleaned_data if post_data is not None else {}
# matching database object exists, update it # matching database object exists, update it
if db_obj is not None and cleaned: if db_obj is not None and cleaned:
if should_delete(cleaned): if should_delete(cleaned):
db_obj.delete() if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
continue # Remove the specific relationship without deleting the object
getattr(db_obj, related_name).remove(self.application)
else:
# If there are no other relationships, delete the object
db_obj.delete()
else: else:
pre_update(db_obj, cleaned) if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
db_obj.save() # create a new db_obj and disconnect existing one
getattr(db_obj, related_name).remove(self.application)
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 # no matching database object, create it
elif db_obj is None and cleaned: # 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) kwargs = pre_create(db_obj, cleaned)
getattr(obj, join).create(**kwargs) getattr(obj, join).create(**kwargs)
@ -170,7 +189,7 @@ class TribalGovernmentForm(RegistrarForm):
) )
tribe_name = forms.CharField( tribe_name = forms.CharField(
label="What is the name of the tribe you represent?", label="Name of tribe",
error_messages={"required": "Enter the tribe you represent."}, error_messages={"required": "Enter the tribe you represent."},
) )
@ -308,13 +327,18 @@ class AboutYourOrganizationForm(RegistrarForm):
class AuthorizingOfficialForm(RegistrarForm): class AuthorizingOfficialForm(RegistrarForm):
JOIN = "authorizing_official"
def to_database(self, obj): def to_database(self, obj):
if not self.is_valid(): if not self.is_valid():
return return
contact = getattr(obj, "authorizing_official", None) contact = getattr(obj, "authorizing_official", None)
if contact is not None: if contact is not None and not contact.has_more_than_one_join("authorizing_official"):
# if contact exists in the database and is not joined to other entities
super().to_database(contact) super().to_database(contact)
else: else:
# no contact exists OR contact exists which is joined also to other entities;
# in either case, create a new contact and update it
contact = Contact() contact = Contact()
super().to_database(contact) super().to_database(contact)
obj.authorizing_official = contact obj.authorizing_official = contact
@ -366,6 +390,8 @@ class BaseCurrentSitesFormSet(RegistrarFormSet):
return website.strip() == "" return website.strip() == ""
def to_database(self, obj: DomainApplication): def to_database(self, obj: DomainApplication):
# If we want to test against multiple joins for a website object, replace the empty array
# and change the JOIN in the models to allow for reverse references
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create) self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
@classmethod @classmethod
@ -384,17 +410,12 @@ CurrentSitesFormSet = forms.formset_factory(
class AlternativeDomainForm(RegistrarForm): class AlternativeDomainForm(RegistrarForm):
def clean_alternative_domain(self): def clean_alternative_domain(self):
"""Validation code for domain names.""" """Validation code for domain names."""
try: requested = self.cleaned_data.get("alternative_domain", None)
requested = self.cleaned_data.get("alternative_domain", None) validated, _ = DraftDomain.validate_and_handle_errors(
validated = DraftDomain.validate(requested, blank_ok=True) domain=requested,
except errors.ExtraDotsError: return_type=ValidationReturnType.FORM_VALIDATION_ERROR,
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots") blank_ok=True,
except errors.DomainUnavailableError: )
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable")
except errors.RegistrySystemError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error")
except ValueError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
return validated return validated
alternative_domain = forms.CharField( alternative_domain = forms.CharField(
@ -423,6 +444,8 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
return {} return {}
def to_database(self, obj: DomainApplication): def to_database(self, obj: DomainApplication):
# If we want to test against multiple joins for a website object, replace the empty array and
# change the JOIN in the models to allow for reverse references
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create) self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
@classmethod @classmethod
@ -469,22 +492,19 @@ class DotGovDomainForm(RegistrarForm):
def clean_requested_domain(self): def clean_requested_domain(self):
"""Validation code for domain names.""" """Validation code for domain names."""
try: requested = self.cleaned_data.get("requested_domain", None)
requested = self.cleaned_data.get("requested_domain", None) validated, _ = DraftDomain.validate_and_handle_errors(
validated = DraftDomain.validate(requested) domain=requested,
except errors.BlankValueError: return_type=ValidationReturnType.FORM_VALIDATION_ERROR,
raise forms.ValidationError(DOMAIN_API_MESSAGES["required"], code="required") )
except errors.ExtraDotsError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots")
except errors.DomainUnavailableError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable")
except errors.RegistrySystemError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error")
except ValueError:
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
return validated return validated
requested_domain = forms.CharField(label="What .gov domain do you want?") requested_domain = forms.CharField(
label="What .gov domain do you want?",
error_messages={
"required": DOMAIN_API_MESSAGES["required"],
},
)
class PurposeForm(RegistrarForm): class PurposeForm(RegistrarForm):
@ -497,18 +517,23 @@ class PurposeForm(RegistrarForm):
message="Response must be less than 1000 characters.", message="Response must be less than 1000 characters.",
) )
], ],
error_messages={"required": "Describe how you'll use the .gov domain youre requesting."}, error_messages={"required": "Describe how youll use the .gov domain youre requesting."},
) )
class YourContactForm(RegistrarForm): class YourContactForm(RegistrarForm):
JOIN = "submitter"
def to_database(self, obj): def to_database(self, obj):
if not self.is_valid(): if not self.is_valid():
return return
contact = getattr(obj, "submitter", None) contact = getattr(obj, "submitter", None)
if contact is not None: if contact is not None and not contact.has_more_than_one_join("submitted_applications"):
# if contact exists in the database and is not joined to other entities
super().to_database(contact) super().to_database(contact)
else: else:
# no contact exists OR contact exists which is joined also to other entities;
# in either case, create a new contact and update it
contact = Contact() contact = Contact()
super().to_database(contact) super().to_database(contact)
obj.submitter = contact obj.submitter = contact
@ -547,6 +572,30 @@ 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 application
if self.application and self.application.has_other_contacts():
initial_value = True
elif self.application and self.application.has_rationale():
initial_value = False
else:
# No pre-selection for new applications
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. (Well ask you to explain why.)")),
initial=initial_value,
widget=forms.RadioSelect,
error_messages={
"required": "This question is required.",
},
)
class OtherContactsForm(RegistrarForm): class OtherContactsForm(RegistrarForm):
first_name = forms.CharField( first_name = forms.CharField(
label="First name / given name", label="First name / given name",
@ -570,7 +619,10 @@ class OtherContactsForm(RegistrarForm):
) )
email = forms.EmailField( email = forms.EmailField(
label="Email", label="Email",
error_messages={"invalid": ("Enter an email address in the required format, like name@example.com.")}, error_messages={
"required": ("Enter an email address in the required format, like name@example.com."),
"invalid": ("Enter an email address in the required format, like name@example.com."),
},
) )
phone = PhoneNumberField( phone = PhoneNumberField(
label="Phone", label="Phone",
@ -580,25 +632,164 @@ class OtherContactsForm(RegistrarForm):
}, },
) )
def __init__(self, *args, **kwargs):
"""
Override the __init__ method for RegistrarForm.
Set form_data_marked_for_deletion to false.
Empty_permitted set to False, as this is overridden in certain circumstances by
Django's BaseFormSet, and results in empty forms being allowed and field level
errors not appropriately raised. This works with code in the view which appropriately
displays required attributes on fields.
"""
self.form_data_marked_for_deletion = False
super().__init__(*args, **kwargs)
self.empty_permitted = False
def mark_form_for_deletion(self):
self.form_data_marked_for_deletion = True
def clean(self): def clean(self):
""" """
This method overrides the default behavior for forms. This method overrides the default behavior for forms.
This cleans the form after field validation has already taken place. This cleans the form after field validation has already taken place.
In this override, allow for a form which is empty to be considered In this override, allow for a form which is deleted by user or marked for
valid even though certain required fields have not passed field deletion by formset to be considered valid even though certain required fields have
validation not passed field validation
"""
if self.form_data_marked_for_deletion or self.cleaned_data.get("DELETE"):
# 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 empty object with only 'delete' attribute defined.
# this will prevent _to_database from creating an empty
# database object
return {"DELETE": True}
return self.cleaned_data
class BaseOtherContactsFormSet(RegistrarFormSet):
"""
FormSet for Other Contacts
There are two conditions by which a form in the formset can be marked for deletion.
One is if the user clicks 'DELETE' button, and this is submitted in the form. The
other is if the YesNo form, which is submitted with this formset, is set to No; in
this case, all forms in formset are marked for deletion. Both of these conditions
must co-exist.
Also, other_contacts have db relationships to multiple db objects. When attempting
to delete an other_contact from an application, those db relationships must be
tested and handled.
"""
JOIN = "other_contacts"
def get_deletion_widget(self):
return forms.HiddenInput(attrs={"class": "deletion"})
def __init__(self, *args, **kwargs):
"""
Override __init__ for RegistrarFormSet.
"""
self.formset_data_marked_for_deletion = False
self.application = kwargs.pop("application", None)
super(RegistrarFormSet, self).__init__(*args, **kwargs)
# quick workaround to ensure that the HTML `required`
# attribute shows up on required fields for the first form
# in the formset plus those that have data already.
for index in range(max(self.initial_form_count(), 1)):
self.forms[index].use_required_attribute = True
def should_delete(self, cleaned):
"""
Implements should_delete method from BaseFormSet.
"""
return self.formset_data_marked_for_deletion or cleaned.get("DELETE", False)
def pre_create(self, db_obj, cleaned):
"""Code to run before an item in the formset is created in the database."""
# remove DELETE from cleaned
if "DELETE" in cleaned:
cleaned.pop("DELETE")
return cleaned
def to_database(self, obj: DomainApplication):
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
@classmethod
def from_database(cls, obj):
return super().from_database(obj, cls.JOIN, cls.on_fetch)
def mark_formset_for_deletion(self):
"""Mark other contacts formset for deletion.
Updates forms in formset as well to mark them for deletion.
This has an effect on validity checks and to_database methods.
"""
self.formset_data_marked_for_deletion = True
for form in self.forms:
form.mark_form_for_deletion()
def is_valid(self):
"""Extend is_valid from RegistrarFormSet. When marking this formset for deletion, set
validate_min to false so that validation does not attempt to enforce a minimum
number of other contacts when contacts marked for deletion"""
if self.formset_data_marked_for_deletion:
self.validate_min = False
return super().is_valid()
OtherContactsFormSet = forms.formset_factory(
OtherContactsForm,
extra=0,
absolute_max=1500, # django default; use `max_num` to limit entries
min_num=1,
can_delete=True,
validate_min=True,
formset=BaseOtherContactsFormSet,
)
class NoOtherContactsForm(RegistrarForm):
no_other_contacts_rationale = forms.CharField(
required=True,
# label has to end in a space to get the label_suffix to show
label=("No other employees rationale"),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
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.
""" """
# Set form_is_empty to True initially if self.form_data_marked_for_deletion:
form_is_empty = True
for name, field in self.fields.items():
# get the value of the field from the widget
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
# if any field in the submitted form is not empty, set form_is_empty to False
if value is not None and value != "":
form_is_empty = False
if form_is_empty:
# clear any errors raised by the form fields # clear any errors raised by the form fields
# (before this clean() method is run, each field # (before this clean() method is run, each field
# performs its own clean, which could result in # performs its own clean, which could result in
@ -612,46 +803,22 @@ class OtherContactsForm(RegistrarForm):
return self.cleaned_data return self.cleaned_data
def to_database(self, obj):
class BaseOtherContactsFormSet(RegistrarFormSet): """
JOIN = "other_contacts" This method overrides the behavior of RegistrarForm.
If form data is marked for deletion, set relevant fields
def should_delete(self, cleaned): to None before saving.
empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values()) Do nothing if form is not valid.
return all(empty) """
if not self.is_valid():
def to_database(self, obj: DomainApplication): return
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create) if self.form_data_marked_for_deletion:
for field_name, _ in self.fields.items():
@classmethod setattr(obj, field_name, None)
def from_database(cls, obj): else:
return super().from_database(obj, cls.JOIN, cls.on_fetch) for name, value in self.cleaned_data.items():
setattr(obj, name, value)
obj.save()
OtherContactsFormSet = forms.formset_factory(
OtherContactsForm,
extra=1,
absolute_max=1500, # django default; use `max_num` to limit entries
formset=BaseOtherContactsFormSet,
)
class NoOtherContactsForm(RegistrarForm):
no_other_contacts_rationale = forms.CharField(
required=True,
# label has to end in a space to get the label_suffix to show
label=(
"Please explain why there are no other employees from your organization "
"we can contact to help us assess your eligibility for a .gov domain."
),
widget=forms.Textarea(),
validators=[
MaxLengthValidator(
1000,
message="Response must be less than 1000 characters.",
)
],
)
class AnythingElseForm(RegistrarForm): class AnythingElseForm(RegistrarForm):

View file

@ -210,6 +210,8 @@ class ContactForm(forms.ModelForm):
class AuthorizingOfficialContactForm(ContactForm): class AuthorizingOfficialContactForm(ContactForm):
"""Form for updating authorizing official contacts.""" """Form for updating authorizing official contacts."""
JOIN = "authorizing_official"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -230,6 +232,29 @@ class AuthorizingOfficialContactForm(ContactForm):
self.fields["email"].error_messages = { self.fields["email"].error_messages = {
"required": "Enter an email address in the required format, like name@example.com." "required": "Enter an email address in the required format, like name@example.com."
} }
self.domainInfo = None
def set_domain_info(self, domainInfo):
"""Set the domain information for the form.
The form instance is associated with the contact itself. In order to access the associated
domain information object, this needs to be set in the form by the view."""
self.domainInfo = domainInfo
def save(self, commit=True):
"""Override the save() method of the BaseModelForm."""
# Get the Contact object from the db for the Authorizing Official
db_ao = Contact.objects.get(id=self.instance.id)
if self.domainInfo and db_ao.has_more_than_one_join("information_authorizing_official"):
# Handle the case where the domain information object is available and the AO Contact
# has more than one joined object.
# In this case, create a new Contact, and update the new Contact with form data.
# Then associate with domain information object as the authorizing_official
data = dict(self.cleaned_data.items())
self.domainInfo.authorizing_official = Contact.objects.create(**data)
self.domainInfo.save()
else:
super().save()
class DomainSecurityEmailForm(forms.Form): class DomainSecurityEmailForm(forms.Form):

View file

@ -0,0 +1,69 @@
""""
Converts all ready and DNS needed domains with a non-default public contact
to disclose their public contact. Created for Issue#1535 to resolve
disclose issue of domains with missing security emails.
"""
import logging
import copy
from django.core.management import BaseCommand
from registrar.models import Domain
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Disclose all nondefault domain security emails."
def __init__(self):
"""Sets global variables for code tidiness"""
super().__init__()
# domains with errors, which are not successfully updated to disclose
self.domains_with_errors: list[str] = []
# domains that are successfully disclosed
self.disclosed_domain_contacts_count = 0
# domains that skip disclose due to having contact registrar@dotgov.gov
self.skipped_domain_contacts_count = 0
def handle(self, **options):
"""
Converts all ready and DNS needed domains with a non-default public contact
to disclose their public contact.
"""
logger.info("Updating security emails to public")
# Initializes domains that need to be disclosed
statuses = [Domain.State.READY, Domain.State.DNS_NEEDED]
domains = Domain.objects.filter(state__in=statuses)
logger.info(f"Found {len(domains)} domains with status Ready or DNS Needed.")
# Update EPP contact for domains with a security contact
for domain in domains:
try:
contact = domain.security_contact # noqa on these items as we only want to call security_contact
logger.info(f"Domain {domain.name} security contact: {domain.security_contact.email}")
if domain.security_contact.email != "registrar@dotgov.gov":
domain._update_epp_contact(contact=domain.security_contact)
self.disclosed_domain_contacts_count += 1
else:
logger.info(
f"Skipping disclose for {domain.name} security contact {domain.security_contact.email}."
)
self.skipped_domain_contacts_count += 1
except Exception as err:
# error condition if domain not in database
self.domains_with_errors.append(copy.deepcopy(domain.name))
logger.error(f"error retrieving domain {domain.name} contact {domain.security_contact}: {err}")
# Inform user how many contacts were disclosed, skipped, and errored
logger.info(f"Updated {self.disclosed_domain_contacts_count} contacts to disclosed.")
logger.info(
f"Skipped disclosing {self.skipped_domain_contacts_count} contacts with security email "
f"registrar@dotgov.gov."
)
logger.info(
f"Error disclosing the following {len(self.domains_with_errors)} contacts: {self.domains_with_errors}"
)

View file

@ -0,0 +1,262 @@
"""Loops through each valid DomainInformation object and updates its agency value"""
import argparse
import csv
import logging
import os
from typing import List
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
from registrar.models.domain_information import DomainInformation
from django.db.models import Q
from registrar.models.transition_domain import TransitionDomain
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Loops through each valid DomainInformation object and updates its agency value"
def __init__(self):
super().__init__()
self.di_to_update: List[DomainInformation] = []
self.di_failed_to_update: List[DomainInformation] = []
self.di_skipped: List[DomainInformation] = []
def add_arguments(self, parser):
"""Adds command line arguments"""
parser.add_argument(
"current_full_filepath",
help="TBD",
)
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument("--sep", default=",", help="Delimiter character")
def handle(self, current_full_filepath, **kwargs):
"""Loops through each valid DomainInformation object and updates its agency value"""
debug = kwargs.get("debug")
separator = kwargs.get("sep")
# Check if the provided file path is valid
if not os.path.isfile(current_full_filepath):
raise argparse.ArgumentTypeError(f"Invalid file path '{current_full_filepath}'")
# === Update the "federal_agency" field === #
was_success = self.patch_agency_info(debug)
# === Try to process anything that was skipped === #
# We should only correct skipped records if the previous step was successful.
# If something goes wrong, then we risk corrupting data, so skip this step.
if len(self.di_skipped) > 0 and was_success:
# Flush out the list of DomainInformations to update
self.di_to_update.clear()
self.process_skipped_records(current_full_filepath, separator, debug)
# Clear the old skipped list, and log the run summary
self.di_skipped.clear()
self.log_script_run_summary(debug)
elif not was_success:
# This code should never execute. This can only occur if bulk_update somehow fails,
# which may indicate some sort of data corruption.
logger.error(
f"{TerminalColors.FAIL}"
"Could not automatically patch skipped records. The initial update failed."
"An error was encountered when running this script, please inspect the following "
f"records for accuracy and completeness: {self.di_failed_to_update}"
f"{TerminalColors.ENDC}"
)
def patch_agency_info(self, debug):
"""
Updates the federal_agency field of each valid DomainInformation object based on the corresponding
TransitionDomain object. Skips the update if the TransitionDomain object does not exist or its
federal_agency field is None. Logs the update, skip, and failure actions if debug mode is on.
After all updates, logs a summary of the results.
"""
# Grab all DomainInformation objects (and their associated TransitionDomains)
# that need to be updated
empty_agency_query = Q(federal_agency=None) | Q(federal_agency="")
domain_info_to_fix = DomainInformation.objects.filter(empty_agency_query)
domain_names = domain_info_to_fix.values_list("domain__name", flat=True)
transition_domains = TransitionDomain.objects.filter(domain_name__in=domain_names).exclude(empty_agency_query)
# Get the domain names from TransitionDomain
td_agencies = transition_domains.values_list("domain_name", "federal_agency").distinct()
human_readable_domain_names = list(domain_names)
# 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 DomainInformation objects to change: {len(human_readable_domain_names)}
The following DomainInformation objects will be modified: {human_readable_domain_names}
""",
prompt_title="Do you wish to patch federal_agency data?",
)
logger.info("Updating...")
# Create a dictionary mapping of domain_name to federal_agency
td_dict = dict(td_agencies)
for di in domain_info_to_fix:
domain_name = di.domain.name
federal_agency = td_dict.get(domain_name)
log_message = None
# If agency exists on a TransitionDomain, update the related DomainInformation object
if domain_name in td_dict:
di.federal_agency = federal_agency
self.di_to_update.append(di)
log_message = f"{TerminalColors.OKCYAN}Updated {di}{TerminalColors.ENDC}"
else:
self.di_skipped.append(di)
log_message = f"{TerminalColors.YELLOW}Skipping update for {di}{TerminalColors.ENDC}"
# Log the action if debug mode is on
if debug and log_message is not None:
logger.info(log_message)
# Bulk update the federal agency field in DomainInformation objects
DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"])
# Get a list of each domain we changed
corrected_domains = DomainInformation.objects.filter(domain__name__in=domain_names)
# After the update has happened, do a sweep of what we get back.
# If the fields we expect to update are still None, then something is wrong.
for di in corrected_domains:
if di not in self.di_skipped and di.federal_agency is None:
logger.info(f"{TerminalColors.FAIL}Failed to update {di}{TerminalColors.ENDC}")
self.di_failed_to_update.append(di)
# === Log results and return data === #
self.log_script_run_summary(debug)
# Tracks if this script was successful. If any errors are found, something went very wrong.
was_success = len(self.di_failed_to_update) == 0
return was_success
def process_skipped_records(self, file_path, separator, debug):
"""If we encounter any DomainInformation records that do not have data in the associated
TransitionDomain record, then check the associated current-full.csv file for this
information."""
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
info_to_inspect=f"""
==File location==
current-full.csv filepath: {file_path}
==Proposed Changes==
Number of DomainInformation objects to change: {len(self.di_skipped)}
The following DomainInformation objects will be modified if agency data exists in file: {self.di_skipped}
""",
prompt_title="Do you wish to patch skipped records?",
)
logger.info("Updating...")
file_data = self.read_current_full(file_path, separator)
for di in self.di_skipped:
domain_name = di.domain.name
row = file_data.get(domain_name)
fed_agency = None
if row is not None and "agency" in row:
fed_agency = row.get("agency")
# Determine if we should update this record or not.
# If we don't get any data back, something went wrong.
if fed_agency is not None:
di.federal_agency = fed_agency
self.di_to_update.append(di)
if debug:
logger.info(f"{TerminalColors.OKCYAN}" f"Updating {di}" f"{TerminalColors.ENDC}")
else:
self.di_failed_to_update.append(di)
logger.error(
f"{TerminalColors.FAIL}" f"Could not update {di}. No information found." f"{TerminalColors.ENDC}"
)
# Bulk update the federal agency field in DomainInformation objects
DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"])
def read_current_full(self, file_path, separator):
"""Reads the current-full.csv file and stores it in a dictionary"""
with open(file_path, "r") as requested_file:
old_reader = csv.DictReader(requested_file, delimiter=separator)
# Some variants of current-full.csv have key casing differences for fields
# such as "Domain name" or "Domain Name". This corrects that.
reader = self.lowercase_fieldnames(old_reader)
# Return a dictionary with the domain name as the key,
# and the row information as the value
dict_data = {}
for row in reader:
domain_name = row.get("domain name")
if domain_name is not None:
domain_name = domain_name.lower()
dict_data[domain_name] = row
return dict_data
def lowercase_fieldnames(self, reader):
"""Lowercases all field keys in a dictreader to account for potential casing differences"""
for row in reader:
yield {k.lower(): v for k, v in row.items()}
def log_script_run_summary(self, debug):
"""Prints success, failed, and skipped counts, as well as
all affected objects."""
update_success_count = len(self.di_to_update)
update_failed_count = len(self.di_failed_to_update)
update_skipped_count = len(self.di_skipped)
# Prepare debug messages
debug_messages = {
"success": (f"{TerminalColors.OKCYAN}Updated: {self.di_to_update}{TerminalColors.ENDC}\n"),
"skipped": (f"{TerminalColors.YELLOW}Skipped: {self.di_skipped}{TerminalColors.ENDC}\n"),
"failed": (f"{TerminalColors.FAIL}Failed: {self.di_failed_to_update}{TerminalColors.ENDC}\n"),
}
# Print out a list of everything that was changed, if we have any changes to log.
# Otherwise, don't print anything.
TerminalHelper.print_conditional(
debug,
f"{debug_messages.get('success') if update_success_count > 0 else ''}"
f"{debug_messages.get('skipped') if update_skipped_count > 0 else ''}"
f"{debug_messages.get('failed') if update_failed_count > 0 else ''}",
)
if update_failed_count == 0 and update_skipped_count == 0:
logger.info(
f"""{TerminalColors.OKGREEN}
============= FINISHED ===============
Updated {update_success_count} DomainInformation entries
{TerminalColors.ENDC}
"""
)
elif update_failed_count == 0:
logger.warning(
f"""{TerminalColors.YELLOW}
============= FINISHED ===============
Updated {update_success_count} DomainInformation entries
----- SOME AGENCY DATA WAS NONE (WILL BE PATCHED AUTOMATICALLY) -----
Skipped updating {update_skipped_count} DomainInformation entries
{TerminalColors.ENDC}
"""
)
else:
logger.error(
f"""{TerminalColors.FAIL}
============= FINISHED ===============
Updated {update_success_count} DomainInformation entries
----- UPDATE FAILED -----
Failed to update {update_failed_count} DomainInformation entries,
Skipped updating {update_skipped_count} DomainInformation entries
{TerminalColors.ENDC}
"""
)

View file

@ -0,0 +1,68 @@
import argparse
import logging
from typing import List
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper, ScriptDataHelper
from registrar.models import Domain
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Loops through each valid Domain object and updates its first_created value"
def __init__(self):
super().__init__()
self.to_update: List[Domain] = []
self.failed_to_update: List[Domain] = []
self.skipped: List[Domain] = []
def add_arguments(self, parser):
"""Adds command line arguments"""
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
def handle(self, **kwargs):
"""Loops through each valid Domain object and updates its first_created value"""
debug = kwargs.get("debug")
# Get all valid domains
valid_states = [Domain.State.READY, Domain.State.ON_HOLD, Domain.State.DELETED]
domains = Domain.objects.filter(first_ready=None, state__in=valid_states)
# 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 Domain objects to change: {len(domains)}
""",
prompt_title="Do you wish to patch first_ready data?",
)
logger.info("Updating...")
for domain in domains:
try:
self.update_first_ready_for_domain(domain, debug)
except Exception as err:
self.failed_to_update.append(domain)
logger.error(err)
logger.error(f"{TerminalColors.FAIL}" f"Failed to update {domain}" f"{TerminalColors.ENDC}")
# Do a bulk update on the first_ready field
ScriptDataHelper.bulk_update_fields(Domain, self.to_update, ["first_ready"])
# Log what happened
TerminalHelper.log_script_run_summary(self.to_update, self.failed_to_update, self.skipped, debug)
def update_first_ready_for_domain(self, domain: Domain, debug: bool):
"""Grabs the created_at field and associates it with the first_ready column.
Appends the result to the to_update list."""
created_at = domain.created_at
if created_at is not None:
domain.first_ready = domain.created_at
self.to_update.append(domain)
if debug:
logger.info(f"Updating {domain}")
else:
self.skipped.append(domain)
if debug:
logger.warning(f"Skipped updating {domain}")

View file

@ -11,6 +11,7 @@ import os
import sys import sys
from typing import Dict, List from typing import Dict, List
from django.core.paginator import Paginator from django.core.paginator import Paginator
from registrar.utility.enums import LogCode
from registrar.models.transition_domain import TransitionDomain from registrar.models.transition_domain import TransitionDomain
from registrar.management.commands.utility.load_organization_error import ( from registrar.management.commands.utility.load_organization_error import (
LoadOrganizationError, LoadOrganizationError,
@ -28,7 +29,8 @@ from .epp_data_containers import (
) )
from .transition_domain_arguments import TransitionDomainArguments from .transition_domain_arguments import TransitionDomainArguments
from .terminal_helper import TerminalColors, TerminalHelper, LogCode from .terminal_helper import TerminalColors, TerminalHelper
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,29 +1,12 @@
from enum import Enum
import logging import logging
import sys import sys
from django.core.paginator import Paginator
from typing import List from typing import List
from registrar.utility.enums import LogCode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LogCode(Enum):
"""Stores the desired log severity
Overview of error codes:
- 1 ERROR
- 2 WARNING
- 3 INFO
- 4 DEBUG
- 5 DEFAULT
"""
ERROR = 1
WARNING = 2
INFO = 3
DEBUG = 4
DEFAULT = 5
class TerminalColors: class TerminalColors:
"""Colors for terminal outputs """Colors for terminal outputs
(makes reading the logs WAY easier)""" (makes reading the logs WAY easier)"""
@ -41,7 +24,94 @@ class TerminalColors:
BackgroundLightYellow = "\033[103m" BackgroundLightYellow = "\033[103m"
class ScriptDataHelper:
"""Helper method with utilities to speed up development of scripts that do DB operations"""
@staticmethod
def bulk_update_fields(model_class, update_list, fields_to_update, batch_size=1000):
"""
This function performs a bulk update operation on a specified Django model class in batches.
It uses Django's Paginator to handle large datasets in a memory-efficient manner.
Parameters:
model_class: The Django model class that you want to perform the bulk update on.
This should be the actual class, not a string of the class name.
update_list: A list of model instances that you want to update. Each instance in the list
should already have the updated values set on the instance.
batch_size: The maximum number of model instances to update in a single database query.
Defaults to 1000. If you're dealing with models that have a large number of fields,
or large field values, you may need to decrease this value to prevent out-of-memory errors.
fields_to_update: Specifies which fields to update.
Usage:
bulk_update_fields(Domain, page.object_list, ["first_ready"])
"""
# Create a Paginator object. Bulk_update on the full dataset
# is too memory intensive for our current app config, so we can chunk this data instead.
paginator = Paginator(update_list, batch_size)
for page_num in paginator.page_range:
page = paginator.page(page_num)
model_class.objects.bulk_update(page.object_list, fields_to_update)
class TerminalHelper: class TerminalHelper:
@staticmethod
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool):
"""Prints success, failed, and skipped counts, as well as
all affected objects."""
update_success_count = len(to_update)
update_failed_count = len(failed_to_update)
update_skipped_count = len(skipped)
# Prepare debug messages
debug_messages = {
"success": (f"{TerminalColors.OKCYAN}Updated: {to_update}{TerminalColors.ENDC}\n"),
"skipped": (f"{TerminalColors.YELLOW}Skipped: {skipped}{TerminalColors.ENDC}\n"),
"failed": (f"{TerminalColors.FAIL}Failed: {failed_to_update}{TerminalColors.ENDC}\n"),
}
# Print out a list of everything that was changed, if we have any changes to log.
# Otherwise, don't print anything.
TerminalHelper.print_conditional(
debug,
f"{debug_messages.get('success') if update_success_count > 0 else ''}"
f"{debug_messages.get('skipped') if update_skipped_count > 0 else ''}"
f"{debug_messages.get('failed') if update_failed_count > 0 else ''}",
)
if update_failed_count == 0 and update_skipped_count == 0:
logger.info(
f"""{TerminalColors.OKGREEN}
============= FINISHED ===============
Updated {update_success_count} entries
{TerminalColors.ENDC}
"""
)
elif update_failed_count == 0:
logger.warning(
f"""{TerminalColors.YELLOW}
============= FINISHED ===============
Updated {update_success_count} entries
----- SOME DATA WAS INVALID (NEEDS MANUAL PATCHING) -----
Skipped updating {update_skipped_count} entries
{TerminalColors.ENDC}
"""
)
else:
logger.error(
f"""{TerminalColors.FAIL}
============= FINISHED ===============
Updated {update_success_count} entries
----- UPDATE FAILED -----
Failed to update {update_failed_count} entries,
Skipped updating {update_skipped_count} entries
{TerminalColors.ENDC}
"""
)
@staticmethod @staticmethod
def query_yes_no(question: str, default="yes"): def query_yes_no(question: str, default="yes"):
"""Ask a yes/no question via raw_input() and return their answer. """Ask a yes/no question via raw_input() and return their answer.

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-09 02:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0061_domain_security_contact_registry_id"),
]
operations = [
migrations.AlterField(
model_name="host",
name="name",
field=models.CharField(default=None, help_text="Fully qualified domain name", max_length=253),
),
]

View file

@ -54,6 +54,47 @@ class Contact(TimeStampedModel):
db_index=True, db_index=True,
) )
def _get_all_relations(self):
"""Returns an array of all fields which are relations"""
return [f.name for f in self._meta.get_fields() if f.is_relation]
def has_more_than_one_join(self, expected_relation):
"""Helper for finding whether an object is joined more than once.
expected_relation is the one relation with one expected join"""
# all_relations is the list of all_relations (from contact) to be checked for existing joins
all_relations = self._get_all_relations()
return any(self._has_more_than_one_join_per_relation(rel, expected_relation) for rel in all_relations)
def _has_more_than_one_join_per_relation(self, relation, expected_relation):
"""Helper for finding whether an object is joined more than once."""
# threshold is the number of related objects that are acceptable
# when determining if related objects exist. threshold is 0 for most
# relationships. if the relationship is expected_relation, we know that
# there is already exactly 1 acceptable relationship (the one we are
# attempting to delete), so the threshold is 1
threshold = 1 if relation == expected_relation else 0
# Raise a KeyError if rel is not a defined field on the db_obj model
# This will help catch any errors in relation passed.
if relation not in [field.name for field in self._meta.get_fields()]:
raise KeyError(f"{relation} is not a defined field on the {self._meta.model_name} model.")
# if attr rel in db_obj is not None, then test if reference object(s) exist
if getattr(self, relation) is not None:
field = self._meta.get_field(relation)
if isinstance(field, models.OneToOneField):
# if the rel field is a OneToOne field, then we have already
# determined that the object exists (is not None)
# so return True unless the relation being tested is the expected_relation
is_not_expected_relation = relation != expected_relation
return is_not_expected_relation
elif isinstance(field, models.ForeignObjectRel):
# if the rel field is a ManyToOne or ManyToMany, then we need
# to determine if the count of related objects is greater than
# the threshold
return getattr(self, relation).count() > threshold
return False
def get_formatted_name(self): def get_formatted_name(self):
"""Returns the contact's name in Western order.""" """Returns the contact's name in Western order."""
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]

View file

@ -1396,11 +1396,13 @@ class Domain(TimeStampedModel, DomainHelper):
def _disclose_fields(self, contact: PublicContact): def _disclose_fields(self, contact: PublicContact):
"""creates a disclose object that can be added to a contact Create using """creates a disclose object that can be added to a contact Create using
.disclose= <this function> on the command before sending. .disclose= <this function> on the command before sending.
if item is security email then make sure email is visable""" if item is security email then make sure email is visible"""
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
DF = epp.DiscloseField DF = epp.DiscloseField
fields = {DF.EMAIL} fields = {DF.EMAIL}
disclose = is_security and contact.email != PublicContact.get_default_security().email disclose = is_security and contact.email != PublicContact.get_default_security().email
# Delete after testing on other devices
logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose)
# Will only disclose DF.EMAIL if its not the default # Will only disclose DF.EMAIL if its not the default
return epp.Disclose( return epp.Disclose(
flag=disclose, flag=disclose,

View file

@ -653,13 +653,11 @@ class DomainApplication(TimeStampedModel):
def in_review(self): def in_review(self):
"""Investigate an application that has been submitted. """Investigate an application that has been submitted.
As a side effect, an email notification is sent.""" This action is logged."""
literal = DomainApplication.ApplicationStatus.IN_REVIEW
self._send_status_update_email( # Check if the tuple exists, then grab its value
"application in review", in_review = literal if literal is not None else "In Review"
"emails/status_change_in_review.txt", logger.info(f"A status change occurred. {self} was changed to '{in_review}'")
"emails/status_change_in_review_subject.txt",
)
@transition( @transition(
field="status", field="status",
@ -674,13 +672,11 @@ class DomainApplication(TimeStampedModel):
def action_needed(self): def action_needed(self):
"""Send back an application that is under investigation or rejected. """Send back an application that is under investigation or rejected.
As a side effect, an email notification is sent.""" This action is logged."""
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
self._send_status_update_email( # Check if the tuple is setup correctly, then grab its value
"action needed", action_needed = literal if literal is not None else "Action Needed"
"emails/status_change_action_needed.txt", logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
"emails/status_change_action_needed_subject.txt",
)
@transition( @transition(
field="status", field="status",
@ -840,9 +836,13 @@ class DomainApplication(TimeStampedModel):
DomainApplication.OrganizationChoices.INTERSTATE, DomainApplication.OrganizationChoices.INTERSTATE,
] ]
def show_no_other_contacts_rationale(self) -> bool: def has_rationale(self) -> bool:
"""Show this step if the other contacts are blank.""" """Does this application have no_other_contacts_rationale?"""
return not self.other_contacts.exists() return bool(self.no_other_contacts_rationale)
def has_other_contacts(self) -> bool:
"""Does this application have other contacts listed?"""
return self.other_contacts.exists()
def is_federal(self) -> Union[bool, None]: def is_federal(self) -> Union[bool, None]:
"""Is this application for a federal agency? """Is this application for a federal agency?

View file

@ -20,7 +20,7 @@ class Host(TimeStampedModel):
null=False, null=False,
blank=False, blank=False,
default=None, # prevent saving without a value default=None, # prevent saving without a value
unique=True, unique=False,
help_text="Fully qualified domain name", help_text="Fully qualified domain name",
) )
@ -30,3 +30,6 @@ class Host(TimeStampedModel):
related_name="host", # access this Host via the Domain as `domain.host` related_name="host", # access this Host via the Domain as `domain.host`
help_text="Domain to which this host belongs", help_text="Domain to which this host belongs",
) )
def __str__(self):
return f"{self.domain.name} {self.name}"

View file

@ -1,8 +1,12 @@
import re import re
from api.views import check_domain_available from django import forms
from django.http import JsonResponse
from api.views import DOMAIN_API_MESSAGES, check_domain_available
from registrar.utility import errors from registrar.utility import errors
from epplibwrapper.errors import RegistryError from epplibwrapper.errors import RegistryError
from registrar.utility.enums import ValidationReturnType
class DomainHelper: class DomainHelper:
@ -23,21 +27,12 @@ class DomainHelper:
return bool(cls.DOMAIN_REGEX.match(domain)) return bool(cls.DOMAIN_REGEX.match(domain))
@classmethod @classmethod
def validate(cls, domain: str | None, blank_ok=False) -> str: def validate(cls, domain: str, blank_ok=False) -> str:
"""Attempt to determine if a domain name could be requested.""" """Attempt to determine if a domain name could be requested."""
if domain is None:
raise errors.BlankValueError() # Split into pieces for the linter
if not isinstance(domain, str): domain = cls._validate_domain_string(domain, blank_ok)
raise ValueError("Domain name must be a string")
domain = domain.lower().strip()
if domain == "" and not blank_ok:
raise errors.BlankValueError()
if domain.endswith(".gov"):
domain = domain[:-4]
if "." in domain:
raise errors.ExtraDotsError()
if not DomainHelper.string_could_be_domain(domain + ".gov"):
raise ValueError()
try: try:
if not check_domain_available(domain): if not check_domain_available(domain):
raise errors.DomainUnavailableError() raise errors.DomainUnavailableError()
@ -45,6 +40,110 @@ class DomainHelper:
raise errors.RegistrySystemError() from err raise errors.RegistrySystemError() from err
return domain return domain
@staticmethod
def _validate_domain_string(domain, blank_ok):
"""Normalize the domain string, and check its content"""
if domain is None:
raise errors.BlankValueError()
if not isinstance(domain, str):
raise errors.InvalidDomainError()
domain = domain.lower().strip()
if domain == "" and not blank_ok:
raise errors.BlankValueError()
elif domain == "":
# If blank ok is true, just return the domain
return domain
if domain.endswith(".gov"):
domain = domain[:-4]
if "." in domain:
raise errors.ExtraDotsError()
if not DomainHelper.string_could_be_domain(domain + ".gov"):
raise errors.InvalidDomainError()
return domain
@classmethod
def validate_and_handle_errors(cls, domain, return_type, blank_ok=False):
"""
Validates a domain and returns an appropriate response based on the validation result.
This method uses the `validate` method to validate the domain. If validation fails, it catches the exception,
maps it to a corresponding error code, and returns a response based on the `return_type` parameter.
Args:
domain (str): The domain to validate.
return_type (ValidationReturnType): Determines the type of response (JSON or form validation error).
blank_ok (bool, optional): If True, blank input does not raise an exception. Defaults to False.
Returns:
tuple: The validated domain (or None if validation failed), and the response (success or error).
""" # noqa
# Map each exception to a corresponding error code
error_map = {
errors.BlankValueError: "required",
errors.ExtraDotsError: "extra_dots",
errors.DomainUnavailableError: "unavailable",
errors.RegistrySystemError: "error",
errors.InvalidDomainError: "invalid",
}
validated = None
response = None
try:
# Attempt to validate the domain
validated = cls.validate(domain, blank_ok)
# Get a list of each possible exception, and the code to return
except tuple(error_map.keys()) as error:
# If an error is caught, get its type
error_type = type(error)
# Generate the response based on the error code and return type
response = DomainHelper._return_form_error_or_json_response(return_type, code=error_map.get(error_type))
else:
# For form validation, we do not need to display the success message
if return_type != ValidationReturnType.FORM_VALIDATION_ERROR:
response = DomainHelper._return_form_error_or_json_response(return_type, code="success", available=True)
# Return the validated domain and the response (either error or success)
return (validated, response)
@staticmethod
def _return_form_error_or_json_response(return_type: ValidationReturnType, code, available=False):
"""
Returns an error response based on the `return_type`.
If `return_type` is `FORM_VALIDATION_ERROR`, raises a form validation error.
If `return_type` is `JSON_RESPONSE`, returns a JSON response with 'available', 'code', and 'message' fields.
If `return_type` is neither, raises a ValueError.
Args:
return_type (ValidationReturnType): The type of error response.
code (str): The error code for the error message.
available (bool, optional): Availability, only used for JSON responses. Defaults to False.
Returns:
A JSON response or a form validation error.
Raises:
ValueError: If `return_type` is neither `FORM_VALIDATION_ERROR` nor `JSON_RESPONSE`.
""" # noqa
match return_type:
case ValidationReturnType.FORM_VALIDATION_ERROR:
raise forms.ValidationError(DOMAIN_API_MESSAGES[code], code=code)
case ValidationReturnType.JSON_RESPONSE:
return JsonResponse({"available": available, "code": code, "message": DOMAIN_API_MESSAGES[code]})
case _:
raise ValueError("Invalid return type specified")
@classmethod @classmethod
def sld(cls, domain: str): def sld(cls, domain: str):
""" """

View file

@ -2,14 +2,16 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Wed like to know more about your organization. Include the following in your response: </p> <p>To help us determine your eligibility for a .gov domain, we need to know more about your organization. For example:</p>
<ul class="usa-list"> <ul class="usa-list">
<li>The type of work your organization does </li> <li>The type of work your organization does </li>
<li>How your organization is a government organization that is independent of a state government </li> <li>How your organization operates independently from a state government</li>
<li>Include links to authorizing legislation, applicable bylaws or charter, or other documentation to support your claims.</li> <li>A description of the specialized, essential services you offer (if applicable)</li>
<li>Links to authorizing legislation, applicable bylaws or charter, or other documentation to support your claims</li>
</ul> </ul>
</p> </p>
<h2>What can you tell us about your organization?</h2>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}
@ -20,4 +22,4 @@
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %} {% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.about_your_organization %} {% input_with_errors forms.0.about_your_organization %}
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}

View file

@ -2,7 +2,9 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Is there anything else you'd like us to know about your domain request? This question is optional.</p> <h2>Is there anything else youd like us to know about your domain request?</h2>
<p>This question is optional.</p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -14,7 +14,7 @@
{% include "includes/ao_example.html" %} {% include "includes/ao_example.html" %}
</div> </div>
<p>We typically dont reach out to the authorizing official, but if contact is necessary, our practice is to coordinate first with you, the requestor. Read more about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p> <p>We typically dont reach out to the authorizing official, but if contact is necessary, our practice is to coordinate with you, the requestor, first.</p>
{% endblock %} {% endblock %}

View file

@ -2,9 +2,9 @@
{% load static field_helpers %} {% load static field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Enter your organizations current public website, if you have one. For example, <p>We can better evaluate your request if we know about domains youre already using.</p>
www.city.com. We can better evaluate your domain request if we know about domains <h2>What are the current websites for your organization?</h2>
youre already using. If you already have any .gov domains please include them. This question is optional.</p> <p>Enter your organizations current public websites. If you already have a .gov domain, include that in your list. This question is optional.</p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -2,24 +2,22 @@
{% load static field_helpers url_helpers %} {% load static field_helpers url_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Before requesting a .gov domain, <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">please make sure it <p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">our naming requirements</a>. Your domain name must:
meets our naming requirements</a>. Your domain name must:
<ul class="usa-list"> <ul class="usa-list">
<li>Be available </li> <li>Be available </li>
<li>Be unique </li>
<li>Relate to your organizations name, location, and/or services </li> <li>Relate to your organizations name, location, and/or services </li>
<li>Be clear to the general public. Your domain name must not be easily confused <li>Be clear to the general public. Your domain name must not be easily confused
with other organizations.</li> with other organizations.</li>
</ul> </ul>
</p> </p>
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations. In most instances, this requires including your states two-letter abbreviation.</p>
<p>Requests for your organizations initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
<p>Note that <strong>only federal agencies can request generic terms</strong> like <p>Note that <strong>only federal agencies can request generic terms</strong> like
vote.gov.</p> vote.gov.</p>
<p>Well try to give you the domain you want. We first need to make sure your request
meets our requirements. Well work with you to find the best domain for your
organization.</p>
<h2 class="margin-top-3">Domain examples for your type of organization</h2> <h2 class="margin-top-3">Domain examples for your type of organization</h2>
<div class="domain_example"> <div class="domain_example">
{% include "includes/domain_example.html" %} {% include "includes/domain_example.html" %}
@ -41,10 +39,7 @@
<h2>What .gov domain do you want?</h2> <h2>What .gov domain do you want?</h2>
</legend> </legend>
<p id="domain_instructions" class="margin-top-05">After you enter your domain, well make sure its <p id="domain_instructions" class="margin-top-05">After you enter your domain, well make sure its available and that it meets some of our naming requirements. If your domain passes these initial checks, well verify that it meets all our requirements after you complete the rest of this form.</p>
available and that it meets some of our naming requirements. If your domain passes
these initial checks, well verify that it meets all of our requirements once you
complete and submit the rest of this form.</p>
{% with attr_aria_describedby="domain_instructions domain_instructions2" %} {% with attr_aria_describedby="domain_instructions domain_instructions2" %}
{# attr_validate / validate="domain" invokes code in get-gov.js #} {# attr_validate / validate="domain" invokes code in get-gov.js #}
@ -53,6 +48,7 @@
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
<button <button
id="check-availability-button"
type="button" type="button"
class="usa-button" class="usa-button"
validate-for="{{ forms.0.requested_domain.auto_id }}" validate-for="{{ forms.0.requested_domain.auto_id }}"
@ -73,9 +69,11 @@
{# attr_validate / validate="domain" invokes code in get-gov.js #} {# attr_validate / validate="domain" invokes code in get-gov.js #}
{# attr_auto_validate likewise triggers behavior in get-gov.js #} {# attr_auto_validate likewise triggers behavior in get-gov.js #}
{% with append_gov=True attr_validate="domain" attr_auto_validate=True %} {% with append_gov=True attr_validate="domain" attr_auto_validate=True %}
{% for form in forms.1 %} {% with add_class="blank-ok alternate-domain-input" %}
{% input_with_errors form.alternative_domain %} {% for form in forms.1 %}
{% endfor %} {% input_with_errors form.alternative_domain %}
{% endfor %}
{% endwith %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
@ -87,6 +85,4 @@
</fieldset> </fieldset>
<p id="domain_instructions2">If youre not sure this is the domain you want, thats
okay. You can change it later.</p>
{% endblock %} {% endblock %}

View file

@ -14,7 +14,7 @@
<h2>Time to complete the form</h2> <h2>Time to complete the form</h2>
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>, <p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
completing your domain request might take around 15 minutes.</p> completing your domain request might take around 15 minutes.</p>
<p><a href="{% public_site_url 'contact/' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Contact us if you need help with your request</a>.</p>
{% block form_buttons %} {% block form_buttons %}
<div class="stepnav"> <div class="stepnav">
@ -29,6 +29,7 @@
</form> </form>
<div class="caption margin-top-5"><a href="{% public_site_url 'privacy-policy#pra' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Paperwork Reduction Act statement</a> (OMB control number: 1670-0049; expiration date: 10/31/2026)</div>
</div> </div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,8 +0,0 @@
{% extends 'application_form.html' %}
{% load static field_helpers %}
{% block form_fields %}
{% with attr_maxlength=1000 %}
{% input_with_errors forms.0.no_other_contacts_rationale %}
{% endwith %}
{% endblock %}

View file

@ -1,16 +1,13 @@
{% extends 'application_form.html' %} {% extends 'application_form.html' %}
{% load field_helpers %} {% load field_helpers url_helpers %}
{% block form_instructions %} {% block form_instructions %}
<h2 class="margin-bottom-05"> <p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="{% public_site_url 'about/data/' %}" target="_blank">.govs public data.</a></p>
What is the name and mailing address of your organization?
</h2>
<p>Enter the name of the organization you represent. Your organization might be part <h2>What is the name and mailing address of the organization you represent?</h2>
of a larger entity. If so, enter information about your part of the larger entity.</p>
<p>Your organization might be part of a larger entity. If so, enter the name of your part of the larger entity. </p>
<p>If your domain request is approved, the name of your organization will be publicly
listed as the domain registrant.</p>
{% endblock %} {% endblock %}
@ -43,4 +40,4 @@
{% input_with_errors forms.0.urbanization %} {% input_with_errors forms.0.urbanization %}
</fieldset> </fieldset>
{% endblock %} {% endblock %}

View file

@ -2,9 +2,11 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<h2 class="margin-bottom-05">Is your organization an election office?</h2>
<p>An election office is a government entity whose <em>primary</em> responsibility is overseeing elections and/or conducting voter registration.</p> <p>An election office is a government entity whose primary responsibility is overseeing elections and/or conducting voter registration. If your organization is an election office, we'll prioritize your request.</p>
<h2>Is your organization an election office?</h2>
<p>Answer “yes” only if the <em>main purpose</em> of your organization is to serve as an election office.</p> <p>Answer “yes” only if the <em>main purpose</em> of your organization is to serve as an election office.</p>

View file

@ -2,49 +2,95 @@
{% load static field_helpers %} {% load static field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>To help us assess your eligibility for a .gov domain, please provide contact information for other employees from your organization. <p>To help us determine your organizations eligibility for a .gov domain, its helpful to have contact information for other employees from your organization.
<ul class="usa-list"> <ul class="usa-list">
<li>They should be clearly and publicly affiliated with your organization and familiar with your domain request. </li> <li>They should be clearly and publicly affiliated with your organization and familiar with your domain request.</li>
<li>They don't need to be involved with the technical management of your domain (although they can be). </li> <li>They dont need to be involved with the technical management of your domain (although they can be).</li>
<li>We typically dont reach out to these employees, but if contact is necessary, our practice is to coordinate first with you, the requestor. </li> <li><strong>We typically dont reach out to these employees</strong>, but if contact is necessary, our practice is to coordinate with you first.</li>
</ul> </ul>
</p> </p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}
{% include "includes/required_fields.html" %} {# commented out so it does not appear at this point on this page #}
{% endblock %} {% endblock %}
{% block form_fields %} {% block form_fields %}
{{ forms.0.management_form }} <fieldset class="usa-fieldset margin-top-2">
{# forms.0 is a formset and this iterates over its forms #}
{% for form in forms.0.forms %}
<fieldset class="usa-fieldset">
<legend> <legend>
<h2>Organization contact {{ forloop.counter }} (optional)</h2> <h2>Are there other employees who can help verify your request?</h2>
</legend> </legend>
{% input_with_errors form.first_name %} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors forms.0.has_other_contacts %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% with add_class="usa-input--medium" %}
{% input_with_errors form.phone %}
{% endwith %} {% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #}
</fieldset> </fieldset>
{% endfor %}
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled"> <div id="other-employees" class="other-contacts-form">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24"> {% include "includes/required_fields.html" %}
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use> {{ forms.1.management_form }}
</svg><span class="margin-left-05">Add another contact</span> {# forms.1 is a formset and this iterates over its forms #}
</button> {% for form in forms.1.forms %}
<fieldset class="usa-fieldset repeatable-form padding-y-1">
<legend class="float-left-tablet">
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
</legend>
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
</svg><span class="margin-left-05">Delete</span>
</button>
{% if forms.1.can_delete %}
{{ form.DELETE }}
{% endif %}
<div class="clear-both">
{% input_with_errors form.first_name %}
</div>
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% comment %} There seems to be an issue with the character counter on emails.
It's not counting anywhere, and in this particular instance it's
affecting the margin of this block. The wrapper div is a
temporary workaround. {% endcomment %}
<div class="margin-top-3">
{% input_with_errors form.email %}
</div>
{% with add_class="usa-input--medium" %}
{% input_with_errors form.phone %}
{% endwith %}
</fieldset>
{% endfor %}
<button type="button" class="usa-button usa-button--unstyled" id="add-form">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
</svg><span class="margin-left-05">Add another contact</span>
</button>
</div>
<div id="no-other-employees">
<fieldset class="usa-fieldset margin-top-4">
<legend>
<h2 class="margin-bottom-0">No other employees from your organization?</h2>
</legend>
<p>You dont need to provide names of other employees now, but it may slow down our assessment of your eligibility. Describe why there are no other employees who can help verify your request.</p>
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.2.no_other_contacts_rationale %}
{% endwith %}
</div>
{% endblock %} {% endblock %}

View file

@ -2,14 +2,10 @@
{% load field_helpers url_helpers %} {% load field_helpers url_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>.Gov domain names are for use on the internet. Dont register a .gov to simply reserve a <p>.Gov domains are intended for public use. Domains will not be given to organizations that only want to reserve a domain name (defensive registration) or that only intend to use the domain internally (as for an intranet).</p>
domain name or for mainly internal use.</p> <p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
<h2>What is the purpose of your requested domain?</h2>
<p>Describe the reason for your domain request. Explain how you plan to use this domain. <p>Describe how youll use your .gov domain. Will it be used for a website, email, or something else?</p>
Who is your intended audience? Will you use it for a website and/or email? Are you moving
your website from another top-level domain (like .com or .org)?
Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -3,51 +3,55 @@
{% block form_instructions %} {% block form_instructions %}
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p> <p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p>
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont examine how government organizations use their domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p> <p>The .gov domain space exists to support a broad diversity of government missions. Generally, we dont review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
<h2>What you cant do with .gov domains</h2> <h2>What you cant do with a .gov domain</h2>
<h3>Commercial purposes</h3> <h3>Commercial purposes</h3>
<p>.Gov domains must not be used for commercial purposes, such as advertising that benefits private individuals or entities.</p> <p>A .gov domain must not be used for commercial purposes, such as advertising that benefits private individuals or entities.</p>
<h3>Political campaigns</h3> <h3>Political campaigns</h3>
<p>.Gov domains must not be used for political campaigns.</p> <p>A .gov domain must not be used for political campaign purposes, such as the website for a candidate seeking elected office.</p>
<h3>Illegal content</h3> <h3>Illegal content</h3>
<p>.Gov domains must not be used to distribute or promote material whose distribution violates applicable law.</p> <p>A .gov domain must not be used to distribute or promote material whose distribution violates applicable law.</p>
<h3>Malicious cyber activity </h3> <h3>Malicious cyber activity </h3>
<p>.Gov is a trusted and safe space. .Gov domains must not distribute malware, host <p>A .gov domain must not distribute malware, host open redirects, or engage in malicious cyber activity.</p>
open redirects, or otherwise engage in malicious cyber activity.</p>
<h2>What .gov domain registrants must do</h2> <h2>What .gov domain registrants must do</h2>
<h3>Keep your contact information updated</h3> <h3>Keep your contact information updated</h3>
<p>.Gov domain registrants must maintain accurate contact information in the .gov registrar.</p> <p>.Gov domain registrants must maintain accurate contact information in the .gov registrar. You will be asked to verify it as part of the renewal process.</p>
<h3>Be responsive if we contact you</h3> <h3>Be responsive if we contact you</h3>
<p>Registrants should respond promptly to communications about potential violations to these requirements.</p> <p>.Gov domain registrants must respond promptly to communications about potential violations to these requirements.</p>
<h2>Failure to comply with these requirements could result in domain suspension or termination</h2> <h2>Failure to comply could result in domain suspension or termination</h2>
<p>We may need to suspend or terminate a domain registration for violations. When we discover a violation, well make reasonable efforts to contact a registrant, including: <p>We may need to suspend or terminate a domain registration for violations of these requirements. When we discover a violation, well make reasonable efforts to contact a registrant, including emails or phone calls to:
<ul class="usa-list"> <ul class="usa-list">
<li>Emails to domain contacts</li> <li>Domain contacts</li>
<li>Phone calls to domain contacts</li> <li>The authorizing official</li>
<li>Email or phone call to the authorizing official</li> <li>The government organization, a parent organization, or affiliated entities</li>
<li>Emails or phone calls to the government organization, a parent organization,
or affiliated entities</li>
</ul> </ul>
</p> </p>
<p>We understand the critical importance of the availability of .gov domains. Suspending or terminating a .gov domain is reserved for prolonged, unresolved, serious violations where the registrant is non-responsive. We'll make extensive efforts to contact registrants and to identify potential solutions. We'll make reasonable accommodations for remediation timelines based on the severity of the issue.</p> <p>We understand the critical importance of availability for a .gov domain. Suspending or terminating a .gov domain is reserved for prolonged, unresolved, serious violations where the registrant is non-responsive. We'll make extensive efforts to contact registrants and to identify potential solutions. We'll make reasonable accommodations for remediation timelines based on the severity of the issue.</p>
<h2>Domain renewal</h2>
<p>.Gov domains are registered for a one-year period. To renew your domain, you'll be asked to verify your organizations eligibility and your contact information. </p>
<p>Though a domain may expire, it will not automatically be put on hold or deleted. Well make extensive efforts to contact your organization before holding or deleting a domain.</p>
{% endblock %} {% endblock %}
{% block form_required_fields_help_text %} {% block form_required_fields_help_text %}

View file

@ -99,16 +99,16 @@
{% if step == Step.OTHER_CONTACTS %} {% if step == Step.OTHER_CONTACTS %}
{% for other in application.other_contacts.all %} {% for other in application.other_contacts.all %}
<div class="margin-bottom-105"> <div class="margin-bottom-105">
<div class="review__step__subheading">Contact {{ forloop.counter }}</div> <p class="text-semibold margin-top-1 margin-bottom-0">Contact {{ forloop.counter }}</p>
{% include "includes/contact.html" with contact=other %} {% include "includes/contact.html" with contact=other %}
</div> </div>
{% empty %} {% empty %}
None <div class="margin-bottom-105">
<p class="text-semibold margin-top-1 margin-bottom-0">No other employees from your organization?</p>
{{ application.no_other_contacts_rationale|default:"Incomplete" }}
</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if step == Step.NO_OTHER_CONTACTS %}
{{ application.no_other_contacts_rationale|default:"Incomplete" }}
{% endif %}
{% if step == Step.ANYTHING_ELSE %} {% if step == Step.ANYTHING_ELSE %}
{{ application.anything_else|default:"No" }} {{ application.anything_else|default:"No" }}
{% endif %} {% endif %}

View file

@ -90,7 +90,7 @@
{% endif %} {% endif %}
{% if domainapplication.current_websites.all %} {% if domainapplication.current_websites.all %}
{% include "includes/summary_item.html" with title='Current website for your organization' value=domainapplication.current_websites.all list='true' heading_level=heading_level %} {% include "includes/summary_item.html" with title='Current websites' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
{% endif %} {% endif %}
{% if domainapplication.requested_domain %} {% if domainapplication.requested_domain %}

View file

@ -1,24 +1,24 @@
{% extends 'application_form.html' %} {% extends 'application_form.html' %}
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %}
<p>To help us determine your eligibility for a .gov domain, we need to know more about your tribal government.</p>
{% endblock %}
{% block form_fields %} {% block form_fields %}
{% with sublabel_text="Please include the entire name of your tribe as recognized by the Bureau of Indian Affairs." %} <h2>What is the name of the tribe you represent?</h2>
{% with link_text="Bureau of Indian Affairs" %} <p>Please include the full name of your tribe as recognized by the <a rel="noopener noreferrer" class="usa-link usa-link--external" href="https://www.federalregister.gov/documents/2024/01/08/2024-00109/indian-entities-recognized-by-and-eligible-to-receive-services-from-the-united-states-bureau-of" target="_blank">Bureau of Indian Affairs</a>.</p>
{% with link_href="https://www.federalregister.gov/documents/2023/01/12/2023-00504/indian-entities-recognized-by-and-eligible-to-receive-services-from-the-united-states-bureau-of" %}
{% with external_link="true" target_blank="true" %} {% with external_link="true" target_blank="true" %}
{% input_with_errors forms.0.tribe_name %} {% input_with_errors forms.0.tribe_name %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %} {% endwith %}
<fieldset class="usa-fieldset"> <fieldset class="usa-fieldset">
<legend class="usa-legend"> <legend class="usa-legend">
<p>Is your organization a federally-recognized tribe or a state-recognized tribe? Check all that apply. <h2>Is your organization a federally-recognized tribe or a state-recognized tribe?</h2>
<abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
</legend> </legend>
<p>Check all that apply. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
{% input_with_errors forms.0.federally_recognized_tribe %} {% input_with_errors forms.0.federally_recognized_tribe %}
{% input_with_errors forms.0.state_recognized_tribe %} {% input_with_errors forms.0.state_recognized_tribe %}
</fieldset> </fieldset>

View file

@ -10,7 +10,7 @@
<h1>Withdraw request for {{ domainapplication.requested_domain.name }}?</h1> <h1>Withdraw request for {{ domainapplication.requested_domain.name }}?</h1>
<p>If you withdraw your request we won't review it. Once you withdraw your request you'll be able to edit it or completely remove it. </p> <p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
<p><a href="{% url 'application-withdrawn' domainapplication.id %}" class="usa-button withdraw">Withdraw request</a> <p><a href="{% url 'application-withdrawn' domainapplication.id %}" class="usa-button withdraw">Withdraw request</a>
<a href="{% url 'application-status' domainapplication.id %}">Cancel</a></p> <a href="{% url 'application-status' domainapplication.id %}">Cancel</a></p>

View file

@ -2,14 +2,11 @@
{% load field_helpers %} {% load field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Well use this information to contact you about your domain request.</p> <p>While reviewing your domain request, we may need to reach out with questions. Well also email you when we complete our review.</p>
<p>If youd like us to use a different name, email, or phone number you can make those <h2>What contact information should we use to reach you?</h2>
changes below. Changing your contact information here wont affect your Login.gov
account information.</p>
<p>The contact information you provide here wont be public and will only be used to <p>Your contact information wont be made public and will be used only for .gov purposes. The information you provide here won't impact your Login.gov account information.</p>
support your domain request.</p>
{% endblock %} {% endblock %}

View file

@ -70,19 +70,6 @@
<script src="{% static 'js/uswds.min.js' %}" defer></script> <script src="{% static 'js/uswds.min.js' %}" defer></script>
<a class="usa-skipnav" href="#main-content">Skip to main content</a> <a class="usa-skipnav" href="#main-content">Skip to main content</a>
{% if IS_DEMO_SITE %}
<section aria-label="Alert" >
<div class="usa-alert usa-alert--info">
<div class="usa-alert__body">
<h4 class="usa-alert__heading">New domain requests are paused</h4>
<p class="usa-alert__text measure-none">
This is the new registrar for managing .gov domains. Note that were not accepting requests for new .gov domains until January 2024. Follow .gov updates at <a href="https://get.gov/updates/" class="usa-link">get.gov/updates/</a>.
</p>
</div>
</div>
</section>
{% endif %}
<section class="usa-banner" aria-label="Official website of the United States government"> <section class="usa-banner" aria-label="Official website of the United States government">
<div class="usa-accordion"> <div class="usa-accordion">
<header class="usa-banner__header"> <header class="usa-banner__header">

View file

@ -1,7 +1,7 @@
{% extends "domain_base.html" %} {% extends "domain_base.html" %}
{% load static field_helpers %} {% load static field_helpers %}
{% block title %}Add another user | {% endblock %} {% block title %}Add a domain manager | {% endblock %}
{% block domain_content %} {% block domain_content %}
<h1>Add a domain manager</h1> <h1>Add a domain manager</h1>

View file

@ -17,9 +17,10 @@
<span class="text-bold text-primary-darker"> <span class="text-bold text-primary-darker">
Status: Status:
</span> </span>
{% if domain.is_expired %} {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
Expired Expired
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%} {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
DNS needed DNS needed
{% else %} {% else %}
{{ domain.state|title }} {{ domain.state|title }}

View file

@ -24,7 +24,7 @@
{% include "includes/required_fields.html" %} {% include "includes/required_fields.html" %}
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container"> <form class="usa-form usa-form--extra-large ds-data-form" method="post" novalidate id="form-container">
{% csrf_token %} {% csrf_token %}
{{ formset.management_form }} {{ formset.management_form }}

View file

@ -24,7 +24,7 @@
{% include "includes/required_fields.html" %} {% include "includes/required_fields.html" %}
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container"> <form class="usa-form usa-form--extra-large nameservers-form" method="post" novalidate id="form-container">
{% csrf_token %} {% csrf_token %}
{{ formset.management_form }} {{ formset.management_form }}

View file

@ -20,7 +20,7 @@
<button <button
type="submit" type="submit"
class="usa-button" class="usa-button"
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov"%}Add security email{% else %}Save{% endif %}</button> >{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov"%}Add security email{% else %}Save{% endif %}</button>
</form> </form>
{% endblock %} {# domain_content #} {% endblock %} {# domain_content #}

View file

@ -3,30 +3,38 @@ Hi.
{{ requester_email }} has added you as a manager on {{ domain.name }}. {{ requester_email }} has added you as a manager on {{ domain.name }}.
You can manage this domain on the .gov registrar <https://manage.get.gov>.
----------------------------------------------------------------
YOU NEED A LOGIN.GOV ACCOUNT YOU NEED A LOGIN.GOV ACCOUNT
Youll need a Login.gov account to manage your .gov domain. Login.gov provides Youll need a Login.gov account to manage your .gov domain. Login.gov provides
a simple and secure process for signing into many government services with one a simple and secure process for signing in to many government services with one
account. If you dont already have one, follow these steps to create your account.
If you dont already have one, follow these steps to create your
Login.gov account <https://login.gov/help/get-started/create-your-account/>. Login.gov account <https://login.gov/help/get-started/create-your-account/>.
DOMAIN MANAGEMENT DOMAIN MANAGEMENT
As a .gov domain manager you can add or update information about your domain. As a .gov domain manager, you can add or update information about your domain.
Youll also serve as a contact for your .gov domain. Please keep your contact Youll also serve as a contact for your .gov domain. Please keep your contact
information updated. Learn more about domain management <https://get.gov/help/>. information updated.
Learn more about domain management <https://get.gov/help/domain-management>.
SOMETHING WRONG? SOMETHING WRONG?
If youre not affiliated with {{ domain.name }} or think you received this If youre not affiliated with {{ domain.name }} or think you received this
message in error, contact the .gov team <https://get.gov/help/#contact-us>. message in error, reply to this email.
THANK YOU THANK YOU
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
.Gov helps the public identify official, trusted information. Thank you for
using a .gov domain.
---------------------------------------------------------------- ----------------------------------------------------------------
The .gov team The .gov team
Contact us: <https://get.gov/contact/> Contact us: <https://get.gov/contact/>
Visit <https://get.gov> Learn about .gov <https://get.gov>
{% endautoescape %} {% endautoescape %}

View file

@ -1,26 +1,28 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}. Hi, {{ application.submitter.first_name }}.
Your .gov domain request has been withdrawn and will not be reviewed by our team.
Your .gov domain request has been withdrawn.
DOMAIN REQUESTED: {{ application.requested_domain.name }} DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST #: {{ application.id }} REQUEST RECEIVED ON: {{ application.submission_date|date }}
STATUS: Withdrawn STATUS: Withdrawn
----------------------------------------------------------------
YOU CAN EDIT YOUR WITHDRAWN REQUEST YOU CAN EDIT YOUR WITHDRAWN REQUEST
You can edit and resubmit this request by signing in to the registrar <https://manage.get.gov/>.
SOMETHING WRONG?
If you didnt ask for this domain request to be withdrawn or think you received this message in error, reply to this email.
The details of your withdrawn request are included below. You can edit and resubmit this application by logging into the registrar. <https://manage.get.gov/>.
THANK YOU THANK YOU
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
---------------------------------------------------------------- ----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team The .gov team
Contact us: <https://get.gov/contact/> Contact us: <https://get.gov/contact/>
Visit <https://get.gov> Learn about .gov <https://get.gov>
{% endautoescape %} {% endautoescape %}

View file

@ -1 +1 @@
Your .gov domain request has been withdrawn Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -17,7 +17,7 @@ About your organization:
Authorizing official: Authorizing official:
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %} {% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
{% if application.current_websites.exists %}{# if block makes a newline #} {% if application.current_websites.exists %}{# if block makes a newline #}
Current website for your organization: {% for site in application.current_websites.all %} Current websites: {% for site in application.current_websites.all %}
{% spaceless %}{{ site.website }}{% endspaceless %} {% spaceless %}{{ site.website }}{% endspaceless %}
{% endfor %}{% endif %} {% endfor %}{% endif %}
.gov domain: .gov domain:

View file

@ -1,42 +0,0 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
We've identified an action needed to complete the review of your .gov domain request.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: Action needed
NEED TO MAKE CHANGES?
If you need to change your request you have to first withdraw it. Once you
withdraw the request you can edit it and submit it again. Changing your request
might add to the wait time. Learn more about withdrawing your request.
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
NEXT STEPS
- You will receive a separate email from our team that provides details about the action needed.
You may need to update your application or provide additional information.
- If you do not receive a separate email with these details within one business day, please contact us:
<https://forms.office.com/pages/responsepage.aspx?id=bOfNPG2UEkq7evydCEI1SqHke9Gh6wJEl3kQ5EjWUKlUQzRJWDlBNTBCQUxTTzBaNlhTWURSSTBLTC4u>
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -1 +0,0 @@
Action needed for your .gov domain request

View file

@ -1,40 +1,51 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}. Hi, {{ application.submitter.first_name }}.
Congratulations! Your .gov domain request has been approved. Congratulations! Your .gov domain request has been approved.
DOMAIN REQUESTED: {{ application.requested_domain.name }} DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }} REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }} STATUS: Approved
STATUS: In review
Now that your .gov domain has been approved, there are a few more things to do before your domain can be used. You can manage your approved domain on the .gov registrar <https://manage.get.gov>.
----------------------------------------------------------------
YOU MUST ADD DOMAIN NAME SERVER INFORMATION ADD DOMAIN NAME SERVER INFORMATION
Before your .gov domain can be used, youll first need to connect it to a Domain Name System (DNS) hosting service. At this time, we dont provide DNS hosting services.
Before your .gov domain can be used, you have to connect it to your Domain Name System (DNS) hosting service. At this time, we dont provide DNS hosting services. After youve set up hosting, youll need to enter your name server information on the .gov registrar.
Go to the domain management page to add your domain name server information <https://manage.get.gov/domain/{{ application.approved_domain.id }}/nameservers>.
Get help with adding your domain name server information <https://get.gov/help/domain-management/#manage-dns-information-for-your-domain>. Learn more about:
- Finding a DNS hosting service <https://get.gov/domains/moving/#find-dns-hosting-services>
- Adding name servers <https://get.gov/help/domain-management/#manage-dns-name-servers>.
ADD DOMAIN MANAGERS, SECURITY EMAIL ADD DOMAIN MANAGERS, SECURITY EMAIL
Currently, youre the only person who can manage this domain. Please keep your contact information updated.
We strongly recommend that you add other points of contact who will help manage your domain. We also recommend that you provide a security email. This email will allow the public to report security issues on your domain. Security emails are made public. We strongly recommend adding other domain managers who can serve as additional contacts. We also recommend providing a security email that the public can use to report security issues on your domain. You can add domain managers and a security email on the .gov registrar.
Go to the domain management page to add domain contacts <https://manage.get.gov/domain/{{ application.approved_domain.id }}/your-contact-information> and a security email <https://manage.get.gov/domain/{{ application.approved_domain.id }}/security-email>. Learn more about:
- Adding domain managers <https://get.gov/help/domain-management/#add-a-domain-manager-to-your-.gov-domain>
- Adding a security email <https://get.gov/help/domain-management/#add-or-update-the-security-email-for-your-.gov-domain>
- Domain security best practices <https://get.gov/domains/security/>
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.
WELL PRELOAD THIS DOMAIN
We add new .gov domains to the HSTS preload list each month. This requires browsers to use a secure HTTPS connection to any website at this domain and ensures the content you publish is exactly what your visitors get. It also means youll need to support HTTPS anywhere the domain is used for websites on the internet or internally. Well add your domain to the preload list soon.
Learn more about:
- What preloading is <https://get.gov/domains/security/#preload-your-domain>
- Why we preload new domains <https://get.gov/posts/2021-06-21-an-intent-to-preload/>
THANK YOU THANK YOU
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. .Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
---------------------------------------------------------------- ----------------------------------------------------------------
The .gov team The .gov team
Contact us: <https://get.gov/contact/> Contact us: <https://get.gov/contact/>
Visit <https://get.gov> Learn about .gov <https://get.gov>
{% endautoescape %} {% endautoescape %}

View file

@ -1 +1 @@
Your .gov domain request is approved Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -1,43 +0,0 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Your .gov domain request is being reviewed.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: In review
NEED TO MAKE CHANGES?
If you need to change your request you have to first withdraw it. Once you
withdraw the request you can edit it and submit it again. Changing your request
might add to the wait time. Learn more about withdrawing your request.
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
NEXT STEPS
- Were reviewing your request. This usually takes 20 business days.
- You can check the status of your request at any time.
<https://manage.get.gov/application/{{ application.id }}>
- Well email you with questions or when we complete our review.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -1 +0,0 @@
Your .gov domain request is being reviewed

View file

@ -1,32 +1,32 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}. Hi, {{ application.submitter.first_name }}.
Your .gov domain request has been rejected. Your .gov domain request has been rejected.
DOMAIN REQUESTED: {{ application.requested_domain.name }} DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }} REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }}
STATUS: Rejected STATUS: Rejected
YOU CAN SUBMIT A NEW REQUEST
The details of your request are included below. If your organization is eligible for a .gov
domain and you meet our other requirements, you can submit a new request. Learn
more about .gov domains <https://get.gov/help/domains/>.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
---------------------------------------------------------------- ----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %} YOU CAN SUBMIT A NEW REQUEST
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
Learn more about:
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
- Choosing a .gov domain name <https://get.gov/domains/choosing>
NEED ASSISTANCE?
If you have questions about this domain request or need help choosing a new domain name, reply to this email.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
---------------------------------------------------------------- ----------------------------------------------------------------
The .gov team The .gov team
Contact us: <https://get.gov/contact/> Contact us: <https://get.gov/contact/>
Visit <https://get.gov> Learn about .gov <https://get.gov>
{% endautoescape %} {% endautoescape %}

View file

@ -1 +1 @@
Your .gov domain request has been rejected Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -1,35 +1,31 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}. Hi, {{ application.submitter.first_name }}.
We received your .gov domain request. We received your .gov domain request.
DOMAIN REQUESTED: {{ application.requested_domain.name }} DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.submission_date|date }} REQUEST RECEIVED ON: {{ application.submission_date|date }}
REQUEST #: {{ application.id }} STATUS: Submitted
STATUS: Received
----------------------------------------------------------------
NEXT STEPS
Well review your request. This usually takes 20 business days. During this review well verify that:
- Your organization is eligible for a .gov domain
- You work at the organization and/or can make requests on its behalf
- Your requested domain meets our naming requirements
Well email you if we have questions and when we complete our review. You can check the status of your request at any time on the registrar homepage. <https://manage.get.gov>
NEED TO MAKE CHANGES? NEED TO MAKE CHANGES?
To make changes to your domain request, you have to withdraw it first. Withdrawing your request may extend the time it takes for the .gov team to complete their review.
If you need to change your request you have to first withdraw it. Once you Learn more about withdrawing your request <https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
withdraw the request you can edit it and submit it again. Changing your request
might add to the wait time. Learn more about withdrawing your request.
NEXT STEPS
- Well review your request. This usually takes 20 business days.
- You can check the status of your request at any time.
<https://manage.get.gov/application/{{ application.id }}>
- Well email you with questions or when we complete our review.
THANK YOU THANK YOU
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
---------------------------------------------------------------- ----------------------------------------------------------------
@ -38,5 +34,5 @@ requesting a .gov domain.
The .gov team The .gov team
Contact us: <https://get.gov/contact/> Contact us: <https://get.gov/contact/>
Visit <https://get.gov> Learn about .gov <https://get.gov>
{% endautoescape %} {% endautoescape %}

View file

@ -1 +1 @@
Thank you for applying for a .gov domain Update on your .gov request: {{ application.requested_domain.name }}

View file

@ -13,22 +13,10 @@
<h1>Manage your domains</h2> <h1>Manage your domains</h2>
<p class="margin-top-4"> <p class="margin-top-4">
{% if IS_PRODUCTION %}
<a href="javascript:void(0)"
class="usa-button usa-tooltip disabled-link"
data-position="right"
title="Coming in 2024"
aria-disabled="true"
data-tooltip="true"
>
Start a new domain request
</a>
{% else %}
<a href="{% url 'application:' %}" class="usa-button" <a href="{% url 'application:' %}" class="usa-button"
> >
Start a new domain request Start a new domain request
</a> </a>
{% endif %}
</p> </p>
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10"> <section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
@ -52,12 +40,13 @@
</th> </th>
<td data-sort-value="{{ domain.expiration_date|date:"U" }}" data-label="Expires">{{ domain.expiration_date|date }}</td> <td data-sort-value="{{ domain.expiration_date|date:"U" }}" data-label="Expires">{{ domain.expiration_date|date }}</td>
<td data-label="Status"> <td data-label="Status">
{% if domain.is_expired %} {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
Expired Expired
{% elif domain.state == "unknown" or domain.state == "dns needed"%} {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
DNS needed DNS needed
{% else %} {% else %}
{{ domain.state|title }} {{ domain.state|capfirst }}
{% endif %} {% endif %}
</td> </td>
<td> <td>

View file

@ -40,8 +40,7 @@
<ul class="usa-list"> <ul class="usa-list">
<li>AmericanSamoa.gov</li> <li>AmericanSamoa.gov</li>
<li>Colorado.gov</li> <li>Colorado.gov</li>
<li>Georgia.gov</li> <li>MN.gov</li>
<li>AmericanSamoa.gov </li>
<li>Guam.gov</li> <li>Guam.gov</li>
</ul> </ul>
@ -55,45 +54,49 @@
</ul> </ul>
{% elif organization_type == 'county' %} {% elif organization_type == 'county' %}
<p>Most county .gov domains must include the two-letter state abbreviation or the full state name. County names that arent shared by any other city, county, parish, town, borough, village or equivalent in the U.S., at the time a domain is granted, can be requested without referring to the state. Counties can include “county” in their domain to distinguish it from other places with similar names. We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if county names are unique.</p> <p>Most county .gov domains must include the two-letter state abbreviation or the full state name. County names that arent shared by any other city, county, parish, town, borough, village or equivalent in the U.S. (at the time a domain is granted) dont have to refer to their state in their domain name. Counties can include “county” in their domain to distinguish it from other places with similar names.</p>
<p>We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if county names are unique.</p>
<p><strong>Examples:</strong></p> <p><strong>Examples:</strong></p>
<ul class="usa-list"> <ul class="usa-list">
<li>AdamsCountyMS.gov</li> <li>LACounty.gov</li>
<li>Erie.gov</li>
<li>LivingstonParishLA.gov</li> <li>LivingstonParishLA.gov</li>
<li>MitchellCountyNC.gov</li> <li>MitchellCountyNC.gov</li>
<li>MiamiDade.gov</li>
</ul> </ul>
{% elif organization_type == 'city' %} {% elif organization_type == 'city' %}
<p>Most city domains must include the two-letter state abbreviation or clearly spell out the state name. Using phrases like “City of” or “Town of” is optional.</p> <p>Most city domains must include the two-letter state abbreviation or clearly spell out the state name. Using phrases like “City of” or “Town of” is optional.</p>
<p>Cities that meet one of the criteria below dont have to refer to their state in the domain name. <p>Cities that meet one of the criteria below dont have to refer to their state in their domain name.
<ul class="usa-list"> <ul class="usa-list">
<li>City names that are not shared by any other U.S. city, town, or village can be requested without referring to the state. We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if names are unique.</li> <li>The city name is not shared by any other U.S. city, town, village, or county. We use the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html">Census Bureaus National Places Gazetteer Files</a> to determine if names are unique.</li>
<li>Certain cities are so well-known that they may not require a state reference to communicate location. We use the list of U.S. “dateline cities” in the Associated Press Stylebook to make this determination.</li> <li>The city is so well known that it doesnt need a state reference to communicate location. We use the list of U.S. “dateline cities” in the Associated Press Stylebook as part of our decision.</li>
<li>The 50 largest cities, as measured by population according to the Census Bureau, can have .gov domain names that dont refer to their state.</li> <li>Its one of the 150 largest cities by population, <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.census.gov/data/tables/time-series/demo/popest/2020s-total-cities-and-towns.html">according to the Census Bureau.</a></li>
</ul> </ul>
<p><strong>Examples:</strong></p> <p><strong>Examples:</strong></p>
<ul class="usa-list"> <ul class="usa-list">
<li>CityofEudoraKS.gov</li> <li>CityofEudoraKS.gov</li>
<li>Pocatello.gov</li>
<li>WallaWallaWA.gov</li> <li>WallaWallaWA.gov</li>
<li>Pocatello.gov</li>
</ul> </ul>
</p> </p>
{% elif organization_type == 'special_district' %} {% elif organization_type == 'special_district' %}
<p>Domain names must represent your organization or institutional name, not solely the services you provide. It also needs to include your two-letter state abbreviation or clearly spell out the state name unless <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing/#counties' %}">county or city exceptions apply</a>.</p> <p>Domain names must represent your organization or institutional name, not solely the services you provide. It also needs to include your two-letter state abbreviation or clearly spell out the state name.</p>
<p><strong>Examples:</strong></p> <p><strong>Examples:</strong></p>
<ul class="usa-list"> <ul class="usa-list">
<li>ElectionsShelbyTN.gov</li>
<li>GlacierViewFire.gov</li> <li>GlacierViewFire.gov</li>
<li>HVcoVote.gov</li>
<li>TechshareTX.gov</li> <li>TechshareTX.gov</li>
<li>UtahTrust.gov</li> <li>UtahTrust.gov</li>
</ul> </ul>
{% elif organization_type == 'school_district' %} {% elif organization_type == 'school_district' %}
<p>Domain names must represent your organization or institutional name.</p> <p>Domain names must represent your organization or institutional name.</p>
<p><strong>Example:</strong> mckinneyISDTX.gov </p> <p><strong>Examples:</strong></p>
<ul class="usa-list">
<li>mckinneyISDTX.gov</li>
<li>BooneCSDIA.gov</li>
</ul>
{%endif %} {%endif %}

View file

@ -743,6 +743,25 @@ class MockEppLib(TestCase):
], ],
) )
mockVerisignDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
"defaultVeri", "registrar@dotgov.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
)
InfoDomainWithVerisignSecurityContact = fakedEppObject(
"fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=[
common.DomainContact(
contact="defaultVeri",
type=PublicContact.ContactTypeChoices.SECURITY,
)
],
hosts=["fake.host.com"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
)
InfoDomainWithDefaultTechnicalContact = fakedEppObject( InfoDomainWithDefaultTechnicalContact = fakedEppObject(
"fakepw", "fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
@ -1058,6 +1077,7 @@ class MockEppLib(TestCase):
"freeman.gov": (self.InfoDomainWithContacts, None), "freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None), "threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None), "defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
"justnameserver.com": (self.justNameserver, None), "justnameserver.com": (self.justNameserver, None),
} }
@ -1087,6 +1107,8 @@ class MockEppLib(TestCase):
mocked_result = self.mockDefaultSecurityContact mocked_result = self.mockDefaultSecurityContact
case "defaultTech": case "defaultTech":
mocked_result = self.mockDefaultTechnicalContact mocked_result = self.mockDefaultTechnicalContact
case "defaultVeri":
mocked_result = self.mockVerisignDataInfoContact
case _: case _:
# Default contact return # Default contact return
mocked_result = self.mockDataInfoContact mocked_result = self.mockDataInfoContact

View file

@ -457,44 +457,6 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching
def test_save_model_sends_in_review_email(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.IN_REVIEW
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "Your .gov domain request is being reviewed."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching @boto3_mocking.patching
def test_save_model_sends_approved_email(self): def test_save_model_sends_approved_email(self):
# make sure there is no user with this email # make sure there is no user with this email
@ -556,44 +518,6 @@ class TestDomainApplicationAdmin(MockEppLib):
# Test that approved domain exists and equals requested domain # Test that approved domain exists and equals requested domain
self.assertEqual(application.requested_domain.name, application.approved_domain.name) self.assertEqual(application.requested_domain.name, application.approved_domain.name)
@boto3_mocking.patching
def test_save_model_sends_action_needed_email(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "We've identified an action needed to complete the review of your .gov domain request."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching @boto3_mocking.patching
def test_save_model_sends_rejected_email(self): def test_save_model_sends_rejected_email(self):
# make sure there is no user with this email # make sure there is no user with this email

View file

@ -47,7 +47,7 @@ class TestEmails(TestCase):
# check for optional things # check for optional things
self.assertIn("Other employees from your organization:", body) self.assertIn("Other employees from your organization:", body)
self.assertIn("Testy2 Tester2", body) self.assertIn("Testy2 Tester2", body)
self.assertIn("Current website for your organization:", body) self.assertIn("Current websites:", body)
self.assertIn("city.com", body) self.assertIn("city.com", body)
self.assertIn("About your organization:", body) self.assertIn("About your organization:", body)
self.assertIn("Anything else", body) self.assertIn("Anything else", body)
@ -61,7 +61,7 @@ class TestEmails(TestCase):
application.submit() application.submit()
_, kwargs = self.mock_client.send_email.call_args _, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertNotIn("Current website for your organization:", body) self.assertNotIn("Current websites:", body)
# spacing should be right between adjacent elements # spacing should be right between adjacent elements
self.assertRegex(body, r"5555\n\n.gov domain:") self.assertRegex(body, r"5555\n\n.gov domain:")
@ -74,9 +74,9 @@ class TestEmails(TestCase):
application.submit() application.submit()
_, kwargs = self.mock_client.send_email.call_args _, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("Current website for your organization:", body) self.assertIn("Current websites:", body)
# spacing should be right between adjacent elements # spacing should be right between adjacent elements
self.assertRegex(body, r"5555\n\nCurrent website for") self.assertRegex(body, r"5555\n\nCurrent websites:")
self.assertRegex(body, r"city.com\n\n.gov domain:") self.assertRegex(body, r"city.com\n\n.gov domain:")
@boto3_mocking.patching @boto3_mocking.patching

View file

@ -1,8 +1,11 @@
"""Test form validation requirements.""" """Test form validation requirements."""
import json
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from api.views import available
from registrar.forms.application_wizard import ( from registrar.forms.application_wizard import (
AlternativeDomainForm,
CurrentSitesForm, CurrentSitesForm,
DotGovDomainForm, DotGovDomainForm,
AuthorizingOfficialForm, AuthorizingOfficialForm,
@ -23,6 +26,7 @@ from django.contrib.auth import get_user_model
class TestFormValidation(MockEppLib): class TestFormValidation(MockEppLib):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.API_BASE_PATH = "/api/v1/available/?domain="
self.user = get_user_model().objects.create(username="username") self.user = get_user_model().objects.create(username="username")
self.factory = RequestFactory() self.factory = RequestFactory()
@ -74,6 +78,113 @@ class TestFormValidation(MockEppLib):
["Enter the .gov domain you want without any periods."], ["Enter the .gov domain you want without any periods."],
) )
def test_requested_domain_errors_consistent(self):
"""Tests if the errors on submit and with the check availability buttons are consistent
for requested_domains
"""
test_cases = [
# extra_dots
("top-level-agency.com", "Enter the .gov domain you want without any periods."),
# invalid
(
"underscores_forever",
"Enter a domain using only letters, numbers, " "or hyphens (though we don't recommend using hyphens).",
),
# required
(
"",
"Enter the .gov domain you want. Dont include “www” or “.gov.”"
" For example, if you want www.city.gov, you would enter “city”"
" (without the quotes).",
),
# unavailable
(
"whitehouse.gov",
"That domain isnt available. <a class='usa-link' "
"href='https://get.gov/domains/choosing' target='_blank'>Read more about "
"choosing your .gov domain</a>.",
),
]
for domain, expected_error in test_cases:
with self.subTest(domain=domain, error=expected_error):
form = DotGovDomainForm(data={"requested_domain": domain})
form_error = list(form.errors["requested_domain"])
# Ensure the form returns what we expect
self.assertEqual(
form_error,
[expected_error],
)
request = self.factory.get(self.API_BASE_PATH + domain)
request.user = self.user
response = available(request, domain=domain)
# Ensure that we're getting the right kind of response
self.assertContains(response, "available")
response_object = json.loads(response.content)
json_error = response_object["message"]
# Test if the message is what we expect
self.assertEqual(json_error, expected_error)
# While its implied,
# for good measure, test if the two objects are equal anyway
self.assertEqual([json_error], form_error)
def test_alternate_domain_errors_consistent(self):
"""Tests if the errors on submit and with the check availability buttons are consistent
for alternative_domains
"""
test_cases = [
# extra_dots
("top-level-agency.com", "Enter the .gov domain you want without any periods."),
# invalid
(
"underscores_forever",
"Enter a domain using only letters, numbers, " "or hyphens (though we don't recommend using hyphens).",
),
# unavailable
(
"whitehouse.gov",
"That domain isnt available. <a class='usa-link' "
"href='https://get.gov/domains/choosing' target='_blank'>Read more about "
"choosing your .gov domain</a>.",
),
]
for domain, expected_error in test_cases:
with self.subTest(domain=domain, error=expected_error):
form = AlternativeDomainForm(data={"alternative_domain": domain})
form_error = list(form.errors["alternative_domain"])
# Ensure the form returns what we expect
self.assertEqual(
form_error,
[expected_error],
)
request = self.factory.get(self.API_BASE_PATH + domain)
request.user = self.user
response = available(request, domain=domain)
# Ensure that we're getting the right kind of response
self.assertContains(response, "available")
response_object = json.loads(response.content)
json_error = response_object["message"]
# Test if the message is what we expect
self.assertEqual(json_error, expected_error)
# While its implied,
# for good measure, test if the two objects are equal anyway
self.assertEqual([json_error], form_error)
def test_requested_domain_two_dots_invalid(self): def test_requested_domain_two_dots_invalid(self):
"""don't accept domains that are subdomains""" """don't accept domains that are subdomains"""
form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"}) form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"})

View file

@ -0,0 +1,499 @@
import copy
import datetime
from django.test import TestCase
from registrar.models import (
User,
Domain,
DomainInvitation,
TransitionDomain,
DomainInformation,
UserDomainRole,
)
from registrar.models.public_contact import PublicContact
from django.core.management import call_command
from unittest.mock import patch, call
from epplibwrapper import commands, common
from .common import MockEppLib
class TestPopulateFirstReady(TestCase):
"""Tests for the populate_first_ready script"""
def setUp(self):
"""Creates a fake domain object"""
super().setUp()
self.ready_domain, _ = Domain.objects.get_or_create(name="fakeready.gov", state=Domain.State.READY)
self.dns_needed_domain, _ = Domain.objects.get_or_create(name="fakedns.gov", state=Domain.State.DNS_NEEDED)
self.deleted_domain, _ = Domain.objects.get_or_create(name="fakedeleted.gov", state=Domain.State.DELETED)
self.hold_domain, _ = Domain.objects.get_or_create(name="fakehold.gov", state=Domain.State.ON_HOLD)
self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN)
# Set a ready_at date for testing purposes
self.ready_at_date = datetime.date(2022, 12, 31)
def tearDown(self):
"""Deletes all DB objects related to migrations"""
super().tearDown()
# Delete domains
Domain.objects.all().delete()
def run_populate_first_ready(self):
"""
This method executes the populate_first_ready command.
The 'call_command' function from Django's management framework is then used to
execute the populate_first_ready command with the specified arguments.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("populate_first_ready")
def test_populate_first_ready_state_ready(self):
"""
Tests that the populate_first_ready works as expected for the state 'ready'
"""
# Set the created at date
self.ready_domain.created_at = self.ready_at_date
self.ready_domain.save()
desired_domain = copy.deepcopy(self.ready_domain)
desired_domain.first_ready = self.ready_at_date
# Run the expiration date script
self.run_populate_first_ready()
self.assertEqual(desired_domain, self.ready_domain)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date)
def test_populate_first_ready_state_deleted(self):
"""
Tests that the populate_first_ready works as expected for the state 'deleted'
"""
# Set the created at date
self.deleted_domain.created_at = self.ready_at_date
self.deleted_domain.save()
desired_domain = copy.deepcopy(self.deleted_domain)
desired_domain.first_ready = self.ready_at_date
# Run the expiration date script
self.run_populate_first_ready()
self.assertEqual(desired_domain, self.deleted_domain)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date)
def test_populate_first_ready_state_dns_needed(self):
"""
Tests that the populate_first_ready doesn't make changes when a domain's state is 'dns_needed'
"""
# Set the created at date
self.dns_needed_domain.created_at = self.ready_at_date
self.dns_needed_domain.save()
desired_domain = copy.deepcopy(self.dns_needed_domain)
desired_domain.first_ready = None
# Run the expiration date script
self.run_populate_first_ready()
current_domain = self.dns_needed_domain
# The object should largely be unaltered (does not test first_ready)
self.assertEqual(desired_domain, current_domain)
first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
# Explicitly test the first_ready date
self.assertNotEqual(first_ready, self.ready_at_date)
self.assertEqual(first_ready, None)
def test_populate_first_ready_state_on_hold(self):
"""
Tests that the populate_first_ready works as expected for the state 'on_hold'
"""
self.hold_domain.created_at = self.ready_at_date
self.hold_domain.save()
desired_domain = copy.deepcopy(self.hold_domain)
desired_domain.first_ready = self.ready_at_date
# Run the update first ready_at script
self.run_populate_first_ready()
current_domain = self.hold_domain
self.assertEqual(desired_domain, current_domain)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date)
def test_populate_first_ready_state_unknown(self):
"""
Tests that the populate_first_ready works as expected for the state 'unknown'
"""
# Set the created at date
self.unknown_domain.created_at = self.ready_at_date
self.unknown_domain.save()
desired_domain = copy.deepcopy(self.unknown_domain)
desired_domain.first_ready = None
# Run the expiration date script
self.run_populate_first_ready()
current_domain = self.unknown_domain
# The object should largely be unaltered (does not test first_ready)
self.assertEqual(desired_domain, current_domain)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
self.assertNotEqual(first_ready, self.ready_at_date)
self.assertEqual(first_ready, None)
class TestPatchAgencyInfo(TestCase):
def setUp(self):
self.user, _ = User.objects.get_or_create(username="testuser")
self.domain, _ = Domain.objects.get_or_create(name="testdomain.gov")
self.domain_info, _ = DomainInformation.objects.get_or_create(domain=self.domain, creator=self.user)
self.transition_domain, _ = TransitionDomain.objects.get_or_create(
domain_name="testdomain.gov", federal_agency="test agency"
)
def tearDown(self):
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
User.objects.all().delete()
TransitionDomain.objects.all().delete()
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True)
def call_patch_federal_agency_info(self, mock_prompt):
"""Calls the patch_federal_agency_info command and mimics a keypress"""
call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True)
def test_patch_agency_info(self):
"""
Tests that the `patch_federal_agency_info` command successfully
updates the `federal_agency` field
of a `DomainInformation` object when the corresponding
`TransitionDomain` object has a valid `federal_agency`.
"""
# Ensure that the federal_agency is None
self.assertEqual(self.domain_info.federal_agency, None)
self.call_patch_federal_agency_info()
# Reload the domain_info object from the database
self.domain_info.refresh_from_db()
# Check that the federal_agency field was updated
self.assertEqual(self.domain_info.federal_agency, "test agency")
def test_patch_agency_info_skip(self):
"""
Tests that the `patch_federal_agency_info` command logs a warning and
does not update the `federal_agency` field
of a `DomainInformation` object when the corresponding
`TransitionDomain` object does not exist.
"""
# Set federal_agency to None to simulate a skip
self.transition_domain.federal_agency = None
self.transition_domain.save()
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
self.call_patch_federal_agency_info()
# Check that the correct log message was output
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
# Reload the domain_info object from the database
self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated
self.assertIsNone(self.domain_info.federal_agency)
def test_patch_agency_info_skip_updates_data(self):
"""
Tests that the `patch_federal_agency_info` command logs a warning but
updates the DomainInformation object, because a record exists in the
provided current-full.csv file.
"""
# Set federal_agency to None to simulate a skip
self.transition_domain.federal_agency = None
self.transition_domain.save()
# Change the domain name to something parsable in the .csv
self.domain.name = "cdomain1.gov"
self.domain.save()
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
self.call_patch_federal_agency_info()
# Check that the correct log message was output
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
# Reload the domain_info object from the database
self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated
self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission")
def test_patch_agency_info_skips_valid_domains(self):
"""
Tests that the `patch_federal_agency_info` command logs INFO and
does not update the `federal_agency` field
of a `DomainInformation` object
"""
self.domain_info.federal_agency = "unchanged"
self.domain_info.save()
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context:
self.call_patch_federal_agency_info()
# Check that the correct log message was output
self.assertIn("FINISHED", context.output[1])
# Reload the domain_info object from the database
self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated
self.assertEqual(self.domain_info.federal_agency, "unchanged")
class TestExtendExpirationDates(MockEppLib):
def setUp(self):
"""Defines the file name of migration_json and the folder its contained in"""
super().setUp()
# Create a valid domain that is updatable
Domain.objects.get_or_create(
name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="testytester@mail.com",
domain_name="waterbutpurple.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with an invalid expiration date
Domain.objects.get_or_create(
name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25)
)
TransitionDomain.objects.get_or_create(
username="themoonisactuallycheese@mail.com",
domain_name="fake.gov",
epp_expiration_date=datetime.date(2022, 5, 25),
)
# Create a domain with an invalid state
Domain.objects.get_or_create(
name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="fakeneeded@mail.com",
domain_name="fakeneeded.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with a date greater than the maximum
Domain.objects.get_or_create(
name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31)
)
TransitionDomain.objects.get_or_create(
username="fakemaximum@mail.com",
domain_name="fakemaximum.gov",
epp_expiration_date=datetime.date(2024, 12, 31),
)
def tearDown(self):
"""Deletes all DB objects related to migrations"""
super().tearDown()
# Delete domain information
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainInvitation.objects.all().delete()
TransitionDomain.objects.all().delete()
# Delete users
User.objects.all().delete()
UserDomainRole.objects.all().delete()
def run_extend_expiration_dates(self):
"""
This method executes the extend_expiration_dates command.
The 'call_command' function from Django's management framework is then used to
execute the extend_expiration_dates command with the specified arguments.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("extend_expiration_dates")
def test_extends_expiration_date_correctly(self):
"""
Tests that the extend_expiration_dates method extends dates as expected
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15))
def test_extends_expiration_date_skips_non_current(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date less than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fake.gov").get()
desired_domain.expiration_date = datetime.date(2022, 5, 25)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fake.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25))
def test_extends_expiration_date_skips_maximum_date(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date more than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
desired_domain.expiration_date = datetime.date(2024, 12, 31)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31))
def test_extends_expiration_date_skips_non_ready(self):
"""
Tests that the extend_expiration_dates method correctly skips domains not in the state "ready"
"""
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
desired_domain.expiration_date = datetime.date(2023, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15))
def test_extends_expiration_date_idempotent(self):
"""
Tests the idempotency of the extend_expiration_dates command.
Verifies that running the method multiple times does not change the expiration date
of a domain beyond the initial extension.
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
# Run the expiration date script again
self.run_extend_expiration_dates()
# The old domain shouldn't have changed
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date - should be the same
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
class TestDiscloseEmails(MockEppLib):
def setUp(self):
super().setUp()
def tearDown(self):
super().tearDown()
PublicContact.objects.all().delete()
Domain.objects.all().delete()
def run_disclose_security_emails(self):
"""
This method executes the disclose_security_emails command.
The 'call_command' function from Django's management framework is then used to
execute the disclose_security_emails command.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("disclose_security_emails")
def test_disclose_security_emails(self):
"""
Tests that command disclose_security_emails runs successfully with
appropriate EPP calll to UpdateContact.
"""
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
expectedSecContact = PublicContact.get_default_security()
expectedSecContact.domain = domain
expectedSecContact.email = "123@mail.gov"
# set domain security email to 123@mail.gov instead of default email
domain.security_contact = expectedSecContact
self.run_disclose_security_emails()
# running disclose_security_emails sends EPP call UpdateContact with disclose
self.mockedSendFunction.assert_has_calls(
[
call(
commands.UpdateContact(
id=domain.security_contact.registry_id,
postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact),
email=domain.security_contact.email,
voice=domain.security_contact.voice,
fax=domain.security_contact.fax,
auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
disclose=domain._disclose_fields(contact=domain.security_contact),
),
cleaned=True,
)
]
)

View file

@ -268,14 +268,12 @@ class TestDomainApplication(TestCase):
(self.ineligible_application, TransitionNotAllowed), (self.ineligible_application, TransitionNotAllowed),
] ]
with boto3_mocking.clients.handler_for("sesv2", self.mock_client): for application, exception_type in test_cases:
with less_console_noise(): with self.subTest(application=application, exception_type=exception_type):
for application, exception_type in test_cases: try:
with self.subTest(application=application, exception_type=exception_type): application.action_needed()
try: except TransitionNotAllowed:
application.action_needed() self.fail("TransitionNotAllowed was raised, but it was not expected.")
except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_action_needed_transition_not_allowed(self): def test_action_needed_transition_not_allowed(self):
""" """
@ -288,12 +286,10 @@ class TestDomainApplication(TestCase):
(self.withdrawn_application, TransitionNotAllowed), (self.withdrawn_application, TransitionNotAllowed),
] ]
with boto3_mocking.clients.handler_for("sesv2", self.mock_client): for application, exception_type in test_cases:
with less_console_noise(): with self.subTest(application=application, exception_type=exception_type):
for application, exception_type in test_cases: with self.assertRaises(exception_type):
with self.subTest(application=application, exception_type=exception_type): application.action_needed()
with self.assertRaises(exception_type):
application.action_needed()
def test_approved_transition_allowed(self): def test_approved_transition_allowed(self):
""" """
@ -500,6 +496,28 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed): with self.assertRaises(TransitionNotAllowed):
self.approved_application.reject_with_prejudice() self.approved_application.reject_with_prejudice()
def test_has_rationale_returns_true(self):
"""has_rationale() returns true when an application has no_other_contacts_rationale"""
self.started_application.no_other_contacts_rationale = "You talkin' to me?"
self.started_application.save()
self.assertEquals(self.started_application.has_rationale(), True)
def test_has_rationale_returns_false(self):
"""has_rationale() returns false when an application has no no_other_contacts_rationale"""
self.assertEquals(self.started_application.has_rationale(), False)
def test_has_other_contacts_returns_true(self):
"""has_other_contacts() returns true when an application has other_contacts"""
# completed_application has other contacts by default
self.assertEquals(self.started_application.has_other_contacts(), True)
def test_has_other_contacts_returns_false(self):
"""has_other_contacts() returns false when an application has no other_contacts"""
application = completed_application(
status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False
)
self.assertEquals(application.has_other_contacts(), False)
class TestPermissions(TestCase): class TestPermissions(TestCase):
"""Test the User-Domain-Role connection.""" """Test the User-Domain-Role connection."""
@ -673,8 +691,12 @@ class TestContact(TestCase):
self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski") self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski")
self.contact, _ = Contact.objects.get_or_create(user=self.user) self.contact, _ = Contact.objects.get_or_create(user=self.user)
self.contact_as_ao, _ = Contact.objects.get_or_create(email="newguy@igorville.gov")
self.application = DomainApplication.objects.create(creator=self.user, authorizing_official=self.contact_as_ao)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
DomainApplication.objects.all().delete()
Contact.objects.all().delete() Contact.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
@ -748,3 +770,12 @@ class TestContact(TestCase):
# Updating the contact's email does not propagate # Updating the contact's email does not propagate
self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com") self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com")
self.assertEqual(self.invalid_user.email, "intern@igorville.gov") self.assertEqual(self.invalid_user.email, "intern@igorville.gov")
def test_has_more_than_one_join(self):
"""Test the Contact model method, has_more_than_one_join"""
# test for a contact which has one user defined
self.assertFalse(self.contact.has_more_than_one_join("user"))
self.assertTrue(self.contact.has_more_than_one_join("authorizing_official"))
# test for a contact which is assigned as an authorizing official on an application
self.assertFalse(self.contact_as_ao.has_more_than_one_join("authorizing_official"))
self.assertTrue(self.contact_as_ao.has_more_than_one_join("submitted_applications"))

View file

@ -4,8 +4,10 @@ from django.test import Client, RequestFactory, TestCase
from io import StringIO from io import StringIO
from registrar.models.domain_information import DomainInformation from registrar.models.domain_information import DomainInformation
from registrar.models.domain import Domain from registrar.models.domain import Domain
from registrar.models.public_contact import PublicContact
from registrar.models.user import User from registrar.models.user import User
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from registrar.tests.common import MockEppLib
from registrar.utility.csv_export import ( from registrar.utility.csv_export import (
write_header, write_header,
write_body, write_body,
@ -221,8 +223,9 @@ class CsvReportsTest(TestCase):
self.assertEqual(expected_file_content, response.content) self.assertEqual(expected_file_content, response.content)
class ExportDataTest(TestCase): class ExportDataTest(MockEppLib):
def setUp(self): def setUp(self):
super().setUp()
username = "test_user" username = "test_user"
first_name = "First" first_name = "First"
last_name = "Last" last_name = "Last"
@ -327,11 +330,85 @@ class ExportDataTest(TestCase):
) )
def tearDown(self): def tearDown(self):
PublicContact.objects.all().delete()
Domain.objects.all().delete() Domain.objects.all().delete()
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
super().tearDown() super().tearDown()
def test_export_domains_to_writer_security_emails(self):
"""Test that export_domains_to_writer returns the
expected security email"""
# Add security email information
self.domain_1.name = "defaultsecurity.gov"
self.domain_1.save()
# Invoke setter
self.domain_1.security_contact
# Invoke setter
self.domain_2.security_contact
# Invoke setter
self.domain_3.security_contact
# Create a CSV file in memory
csv_file = StringIO()
writer = csv.writer(csv_file)
# Define columns, sort fields, and filter condition
columns = [
"Domain name",
"Domain type",
"Agency",
"Organization name",
"City",
"State",
"AO",
"AO email",
"Security contact email",
"Status",
"Expiration date",
]
sort_fields = ["domain__name"]
filter_condition = {
"domain__state__in": [
Domain.State.READY,
Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD,
],
}
self.maxDiff = None
# Call the export functions
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
# We expect READY domains,
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
"AO email,Security contact email,Status,Expiration date\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
"adomain2.gov,Interstate,(blank),Dns needed\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
def test_write_body(self): def test_write_body(self):
"""Test that write_body returns the """Test that write_body returns the
existing domain, test that sort by domain name works, existing domain, test that sort by domain name works,

View file

@ -18,178 +18,10 @@ from unittest.mock import patch
from registrar.models.contact import Contact from registrar.models.contact import Contact
from .common import MockEppLib, MockSESClient, less_console_noise from .common import MockSESClient, less_console_noise
import boto3_mocking # type: ignore import boto3_mocking # type: ignore
class TestExtendExpirationDates(MockEppLib):
def setUp(self):
"""Defines the file name of migration_json and the folder its contained in"""
super().setUp()
# Create a valid domain that is updatable
Domain.objects.get_or_create(
name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="testytester@mail.com",
domain_name="waterbutpurple.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with an invalid expiration date
Domain.objects.get_or_create(
name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25)
)
TransitionDomain.objects.get_or_create(
username="themoonisactuallycheese@mail.com",
domain_name="fake.gov",
epp_expiration_date=datetime.date(2022, 5, 25),
)
# Create a domain with an invalid state
Domain.objects.get_or_create(
name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="fakeneeded@mail.com",
domain_name="fakeneeded.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with a date greater than the maximum
Domain.objects.get_or_create(
name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31)
)
TransitionDomain.objects.get_or_create(
username="fakemaximum@mail.com",
domain_name="fakemaximum.gov",
epp_expiration_date=datetime.date(2024, 12, 31),
)
def tearDown(self):
"""Deletes all DB objects related to migrations"""
super().tearDown()
# Delete domain information
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainInvitation.objects.all().delete()
TransitionDomain.objects.all().delete()
# Delete users
User.objects.all().delete()
UserDomainRole.objects.all().delete()
def run_extend_expiration_dates(self):
"""
This method executes the transfer_transition_domains_to_domains command.
The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("extend_expiration_dates")
def test_extends_expiration_date_correctly(self):
"""
Tests that the extend_expiration_dates method extends dates as expected
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15))
def test_extends_expiration_date_skips_non_current(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date less than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fake.gov").get()
desired_domain.expiration_date = datetime.date(2022, 5, 25)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fake.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25))
def test_extends_expiration_date_skips_maximum_date(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date more than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
desired_domain.expiration_date = datetime.date(2024, 12, 31)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31))
def test_extends_expiration_date_skips_non_ready(self):
"""
Tests that the extend_expiration_dates method correctly skips domains not in the state "ready"
"""
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
desired_domain.expiration_date = datetime.date(2023, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15))
def test_extends_expiration_date_idempotent(self):
"""
Tests the idempotency of the extend_expiration_dates command.
Verifies that running the method multiple times does not change the expiration date
of a domain beyond the initial extension.
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
# Run the expiration date script again
self.run_extend_expiration_dates()
# The old domain shouldn't have changed
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date - should be the same
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
class TestProcessedMigrations(TestCase): class TestProcessedMigrations(TestCase):
"""This test case class is designed to verify the idempotency of migrations """This test case class is designed to verify the idempotency of migrations
related to domain transitions in the application.""" related to domain transitions in the application."""

File diff suppressed because it is too large Load diff

View file

@ -26,12 +26,23 @@ def get_domain_infos(filter_condition, sort_fields):
def write_row(writer, columns, domain_info): def write_row(writer, columns, domain_info):
security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
# For linter # For linter
ao = " " ao = " "
if domain_info.authorizing_official: if domain_info.authorizing_official:
first_name = domain_info.authorizing_official.first_name or "" first_name = domain_info.authorizing_official.first_name or ""
last_name = domain_info.authorizing_official.last_name or "" last_name = domain_info.authorizing_official.last_name or ""
ao = first_name + " " + last_name ao = first_name + " " + last_name
security_email = " "
if security_contacts:
security_email = security_contacts[0].email
invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"}
# These are default emails that should not be displayed in the csv report
if security_email is not None and security_email.lower() in invalid_emails:
security_email = "(blank)"
# create a dictionary of fields which can be included in output # create a dictionary of fields which can be included in output
FIELDS = { FIELDS = {
"Domain name": domain_info.domain.name, "Domain name": domain_info.domain.name,
@ -44,13 +55,14 @@ def write_row(writer, columns, domain_info):
"State": domain_info.state_territory, "State": domain_info.state_territory,
"AO": ao, "AO": ao,
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ", "AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
"Security contact email": security_contacts[0].email if security_contacts else " ", "Security contact email": security_email,
"Status": domain_info.domain.get_state_display(), "Status": domain_info.domain.get_state_display(),
"Expiration date": domain_info.domain.expiration_date, "Expiration date": domain_info.domain.expiration_date,
"Created at": domain_info.domain.created_at, "Created at": domain_info.domain.created_at,
"First ready": domain_info.domain.first_ready, "First ready": domain_info.domain.first_ready,
"Deleted": domain_info.domain.deleted, "Deleted": domain_info.domain.deleted,
} }
writer.writerow([FIELDS.get(column, "") for column in columns]) writer.writerow([FIELDS.get(column, "") for column in columns])

View file

@ -0,0 +1,28 @@
"""Used for holding various enums"""
from enum import Enum
class ValidationReturnType(Enum):
"""Determines the return value of the validate_and_handle_errors class"""
JSON_RESPONSE = "JSON_RESPONSE"
FORM_VALIDATION_ERROR = "FORM_VALIDATION_ERROR"
class LogCode(Enum):
"""Stores the desired log severity
Overview of error codes:
- 1 ERROR
- 2 WARNING
- 3 INFO
- 4 DEBUG
- 5 DEFAULT
"""
ERROR = 1
WARNING = 2
INFO = 3
DEBUG = 4
DEFAULT = 5

View file

@ -17,6 +17,12 @@ class RegistrySystemError(ValueError):
pass pass
class InvalidDomainError(ValueError):
"""Error class for situations where an invalid domain is supplied"""
pass
class ActionNotAllowed(Exception): class ActionNotAllowed(Exception):
"""User accessed an action that is not """User accessed an action that is not
allowed by the current state""" allowed by the current state"""

View file

@ -42,7 +42,6 @@ class Step(StrEnum):
PURPOSE = "purpose" PURPOSE = "purpose"
YOUR_CONTACT = "your_contact" YOUR_CONTACT = "your_contact"
OTHER_CONTACTS = "other_contacts" OTHER_CONTACTS = "other_contacts"
NO_OTHER_CONTACTS = "no_other_contacts"
ANYTHING_ELSE = "anything_else" ANYTHING_ELSE = "anything_else"
REQUIREMENTS = "requirements" REQUIREMENTS = "requirements"
REVIEW = "review" REVIEW = "review"
@ -74,7 +73,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
URL_NAMESPACE = "application" URL_NAMESPACE = "application"
# name for accessing /application/<id>/edit # name for accessing /application/<id>/edit
EDIT_URL_NAME = "edit-application" EDIT_URL_NAME = "edit-application"
NEW_URL_NAME = "/register/" NEW_URL_NAME = "/request/"
# We need to pass our human-readable step titles as context to the templates. # We need to pass our human-readable step titles as context to the templates.
TITLES = { TITLES = {
Step.ORGANIZATION_TYPE: _("Type of organization"), Step.ORGANIZATION_TYPE: _("Type of organization"),
@ -84,12 +83,11 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"), Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"), Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
Step.AUTHORIZING_OFFICIAL: _("Authorizing official"), Step.AUTHORIZING_OFFICIAL: _("Authorizing official"),
Step.CURRENT_SITES: _("Current website for your organization"), Step.CURRENT_SITES: _("Current websites"),
Step.DOTGOV_DOMAIN: _(".gov domain"), Step.DOTGOV_DOMAIN: _(".gov domain"),
Step.PURPOSE: _("Purpose of your domain"), Step.PURPOSE: _("Purpose of your domain"),
Step.YOUR_CONTACT: _("Your contact information"), Step.YOUR_CONTACT: _("Your contact information"),
Step.OTHER_CONTACTS: _("Other employees from your organization"), Step.OTHER_CONTACTS: _("Other employees from your organization"),
Step.NO_OTHER_CONTACTS: _("No other employees from your organization?"),
Step.ANYTHING_ELSE: _("Anything else?"), Step.ANYTHING_ELSE: _("Anything else?"),
Step.REQUIREMENTS: _("Requirements for operating .gov domains"), Step.REQUIREMENTS: _("Requirements for operating .gov domains"),
Step.REVIEW: _("Review and submit your domain request"), Step.REVIEW: _("Review and submit your domain request"),
@ -102,7 +100,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
Step.TRIBAL_GOVERNMENT: lambda w: w.from_model("show_tribal_government", False), Step.TRIBAL_GOVERNMENT: lambda w: w.from_model("show_tribal_government", False),
Step.ORGANIZATION_ELECTION: lambda w: w.from_model("show_organization_election", False), Step.ORGANIZATION_ELECTION: lambda w: w.from_model("show_organization_election", False),
Step.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model("show_about_your_organization", False), Step.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model("show_about_your_organization", False),
Step.NO_OTHER_CONTACTS: lambda w: w.from_model("show_no_other_contacts_rationale", False),
} }
def __init__(self): def __init__(self):
@ -153,6 +150,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
def storage(self): def storage(self):
# marking session as modified on every access # marking session as modified on every access
# so that updates to nested keys are always saved # so that updates to nested keys are always saved
# push to sandbox will remove
self.request.session.modified = True self.request.session.modified = True
return self.request.session.setdefault(self.prefix, {}) return self.request.session.setdefault(self.prefix, {})
@ -488,12 +486,46 @@ class YourContact(ApplicationWizard):
class OtherContacts(ApplicationWizard): class OtherContacts(ApplicationWizard):
template_name = "application_other_contacts.html" template_name = "application_other_contacts.html"
forms = [forms.OtherContactsFormSet] forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
def is_valid(self, forms: list) -> bool:
"""Overrides default behavior defined in ApplicationWizard.
Depending on value in other_contacts_yes_no_form, marks forms in
other_contacts or no_other_contacts for deletion. Then validates
all forms.
"""
other_contacts_yes_no_form = forms[0]
other_contacts_forms = forms[1]
no_other_contacts_form = forms[2]
class NoOtherContacts(ApplicationWizard): # set all the required other_contact fields as necessary since new forms
template_name = "application_no_other_contacts.html" # were added through javascript
forms = [forms.NoOtherContactsForm] for form in forms[1].forms:
for field_item, field in form.fields.items():
if field.required:
field.widget.attrs["required"] = "required"
all_forms_valid = True
# test first for yes_no_form validity
if other_contacts_yes_no_form.is_valid():
# test for has_contacts
if other_contacts_yes_no_form.cleaned_data.get("has_other_contacts"):
# mark the no_other_contacts_form for deletion
no_other_contacts_form.mark_form_for_deletion()
# test that the other_contacts_forms and no_other_contacts_forms are valid
all_forms_valid = all(form.is_valid() for form in forms[1:])
else:
# mark the other_contacts_forms formset for deletion
other_contacts_forms.mark_formset_for_deletion()
all_forms_valid = all(form.is_valid() for form in forms[1:])
else:
# if yes no form is invalid, no choice has been made
# mark other forms for deletion so that their errors are not
# returned
other_contacts_forms.mark_formset_for_deletion()
no_other_contacts_form.mark_form_for_deletion()
all_forms_valid = False
return all_forms_valid
class AnythingElse(ApplicationWizard): class AnythingElse(ApplicationWizard):

View file

@ -222,6 +222,10 @@ class DomainAuthorizingOfficialView(DomainFormBaseView):
def form_valid(self, form): def form_valid(self, form):
"""The form is valid, save the authorizing official.""" """The form is valid, save the authorizing official."""
# Set the domain information in the form so that it can be accessible
# to associate a new Contact as authorizing official, if new Contact is needed
# in the save() method
form.set_domain_info(self.object.domain_info)
form.save() form.save()
messages.success(self.request, "The authorizing official for this domain has been updated.") messages.success(self.request, "The authorizing official for this domain has been updated.")
@ -564,7 +568,9 @@ class DomainSecurityEmailView(DomainFormBaseView):
"""The initial value for the form.""" """The initial value for the form."""
initial = super().get_initial() initial = super().get_initial()
security_contact = self.object.security_contact security_contact = self.object.security_contact
if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov":
invalid_emails = ["dotgov@cisa.dhs.gov", "registrar@dotgov.gov"]
if security_contact is None or security_contact.email in invalid_emails:
initial["security_email"] = None initial["security_email"] = None
return initial return initial
initial["security_email"] = security_contact.email initial["security_email"] = security_contact.email