mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-03 09:43:33 +02:00
Merge branch 'main' into za/1523-inconsistent-error-messages
This commit is contained in:
commit
585fefce88
62 changed files with 2261 additions and 753 deletions
|
@ -2,7 +2,7 @@
|
|||
|
||||
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.
|
||||
|
||||
2. The analyst approves the application using the `DomainApplication`'s
|
||||
|
@ -139,7 +139,7 @@ DomainInvitation -- Domain
|
|||
DomainInvitation .[#green].> UserDomainRole : User.on_each_login()
|
||||
|
||||
actor applicant #Red
|
||||
applicant -d-> DomainApplication : **/register**
|
||||
applicant -d-> DomainApplication : **/request**
|
||||
|
||||
actor analyst #Blue
|
||||
analyst -[#blue]-> DomainApplication : **approve()**
|
||||
|
|
|
@ -21,28 +21,6 @@
|
|||
- 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)
|
||||
|
||||
## 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
|
||||
- Starting Location: Django Admin
|
||||
- Workflow: Analyst Admin
|
||||
|
|
|
@ -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 ssh getgov-<app>`
|
||||
- `/tmp/lifecycle/shell`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 39_previous_miration --fake' --name migrate`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 41_example_migration' --name migrate`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 45_last_migration --fake' --name migrate`
|
||||
|
||||
Then, navigate to and delete the offending migration. In this case, it is 0041_example_migration.
|
||||
- Find the conflicting migrations: `./manage.py showmigrations`
|
||||
- Delete one of them: `rm registrar/migrations/0041_example.py`
|
||||
- `/manage.py showmigrations`
|
||||
- `/manage.py makemigrations`
|
||||
- `/manage.py migrate`
|
||||
|
||||
### 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`
|
||||
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}`
|
||||
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`
|
||||
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 you’re starting to lose your marbles.
|
||||
|
||||
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, there’s the error!
|
||||
Essentially this shows that your requests were being handled by two completely separate applications and that’s why some requests aren’t being located.
|
||||
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: Can’t log into sandbox, permissions do not exist
|
||||
|
||||
- Fake migrate the migration that’s before the last data creation migration
|
||||
- Run the last data creation migration (AND ONLY THAT ONE)
|
||||
- Fake migrate the last migration in the migration list
|
||||
- Rerun fixtures
|
||||
1. `./manage.py migrate --fake model_name_here file_name_BEFORE_the_most_recent_CREATE_migration` (fake migrate the migration that’s before the last data creation migration -- look for number_create, and then copy the file BEFORE it)
|
||||
2. `./manage.py migrate model_name_here file_name_WITH_create` (run the last data creation migration AND ONLY THAT ONE)
|
||||
3. `./manage.py migrate --fake model_name_here most_recent_file_name` (fake migrate the last migration in the migration list)
|
||||
4. `./manage.py load` (rerun fixtures)
|
||||
|
|
|
@ -35,17 +35,55 @@ Binding the database in `manifest-<ENVIRONMENT>.json` automatically inserts the
|
|||
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -524,3 +524,65 @@ Example: `cf ssh getgov-za`
|
|||
| 2 | **debug** | Increases logging detail. Defaults to False. |
|
||||
| 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. |
|
||||
|
||||
|
||||
## 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. |
|
||||
|
|
28
src/.pa11yci
28
src/.pa11yci
|
@ -6,19 +6,19 @@
|
|||
"urls": [
|
||||
"http://localhost:8080/",
|
||||
"http://localhost:8080/health/",
|
||||
"http://localhost:8080/register/",
|
||||
"http://localhost:8080/register/organization/",
|
||||
"http://localhost:8080/register/org_federal/",
|
||||
"http://localhost:8080/register/org_election/",
|
||||
"http://localhost:8080/register/org_contact/",
|
||||
"http://localhost:8080/register/authorizing_official/",
|
||||
"http://localhost:8080/register/current_sites/",
|
||||
"http://localhost:8080/register/dotgov_domain/",
|
||||
"http://localhost:8080/register/purpose/",
|
||||
"http://localhost:8080/register/your_contact/",
|
||||
"http://localhost:8080/register/other_contacts/",
|
||||
"http://localhost:8080/register/anything_else/",
|
||||
"http://localhost:8080/register/requirements/",
|
||||
"http://localhost:8080/register/finished/"
|
||||
"http://localhost:8080/request/",
|
||||
"http://localhost:8080/request/organization/",
|
||||
"http://localhost:8080/request/org_federal/",
|
||||
"http://localhost:8080/request/org_election/",
|
||||
"http://localhost:8080/request/org_contact/",
|
||||
"http://localhost:8080/request/authorizing_official/",
|
||||
"http://localhost:8080/request/current_sites/",
|
||||
"http://localhost:8080/request/dotgov_domain/",
|
||||
"http://localhost:8080/request/purpose/",
|
||||
"http://localhost:8080/request/your_contact/",
|
||||
"http://localhost:8080/request/other_contacts/",
|
||||
"http://localhost:8080/request/anything_else/",
|
||||
"http://localhost:8080/request/requirements/",
|
||||
"http://localhost:8080/request/finished/"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ DOMAIN_API_MESSAGES = {
|
|||
"unavailable": mark_safe( # nosec
|
||||
"That domain isn’t available. "
|
||||
"<a class='usa-link' href='{}' target='_blank'>"
|
||||
"Read more about choosing your .gov domain.</a>".format(public_site_url("domains/choosing"))
|
||||
"Read more about choosing your .gov domain</a>.".format(public_site_url("domains/choosing"))
|
||||
),
|
||||
"invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
|
||||
"success": "That domain is available! We’ll try to give you the domain you want, \
|
||||
|
|
|
@ -51,7 +51,7 @@ class ViewsTest(TestCase):
|
|||
# assert
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertTemplateUsed(response, "500.html")
|
||||
self.assertIn("server error", response.content.decode("utf-8"))
|
||||
self.assertIn("Server error", response.content.decode("utf-8"))
|
||||
|
||||
def test_login_callback_reads_next(self, mock_client):
|
||||
# setup
|
||||
|
|
|
@ -401,6 +401,8 @@ class HostIPInline(admin.StackedInline):
|
|||
class MyHostAdmin(AuditedAdmin):
|
||||
"""Custom host admin class to use our inlines."""
|
||||
|
||||
search_fields = ["name", "domain__name"]
|
||||
search_help_text = "Search by domain or hostname."
|
||||
inlines = [HostIPInline]
|
||||
|
||||
|
||||
|
@ -1251,8 +1253,8 @@ admin.site.register(models.DomainInformation, DomainInformationAdmin)
|
|||
admin.site.register(models.Domain, DomainAdmin)
|
||||
admin.site.register(models.DraftDomain, DraftDomainAdmin)
|
||||
# Host and HostIP removed from django admin because changes in admin
|
||||
# do not propogate to registry and logic not applied
|
||||
# admin.site.register(models.Host, MyHostAdmin)
|
||||
# do not propagate to registry and logic not applied
|
||||
admin.site.register(models.Host, MyHostAdmin)
|
||||
admin.site.register(models.Website, WebsiteAdmin)
|
||||
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||
|
|
|
@ -666,3 +666,5 @@ function toggleTwoDomElements(ele1, ele2, index) {
|
|||
handleRadioButtonChange();
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
@include sr-only;
|
||||
}
|
||||
|
||||
.clear-both {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
|
|
@ -31,3 +31,10 @@
|
|||
padding-left: 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
legend.float-left-tablet + button.float-right-tablet {
|
||||
margin-top: .5rem;
|
||||
@include at-media('tablet') {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ for step, view in [
|
|||
(Step.PURPOSE, views.Purpose),
|
||||
(Step.YOUR_CONTACT, views.YourContact),
|
||||
(Step.OTHER_CONTACTS, views.OtherContacts),
|
||||
(Step.NO_OTHER_CONTACTS, views.NoOtherContacts),
|
||||
(Step.ANYTHING_ELSE, views.AnythingElse),
|
||||
(Step.REQUIREMENTS, views.Requirements),
|
||||
(Step.REVIEW, views.Review),
|
||||
|
@ -77,7 +76,7 @@ urlpatterns = [
|
|||
),
|
||||
path("health/", views.health),
|
||||
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/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"),
|
||||
|
|
|
@ -8,6 +8,7 @@ from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
|||
from django import forms
|
||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.db.models.fields.related import ForeignObjectRel, OneToOneField
|
||||
|
||||
from registrar.models import Contact, DomainApplication, DraftDomain, Domain
|
||||
from registrar.templatetags.url_helpers import public_site_url
|
||||
|
@ -94,10 +95,39 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def has_more_than_one_join(self, db_obj, rel, related_name):
|
||||
"""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 related_name, 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 rel == related_name else 0
|
||||
|
||||
# Raise a KeyError if rel is not a defined field on the db_obj model
|
||||
# This will help catch any errors in reverse_join config on forms
|
||||
if rel not in [field.name for field in db_obj._meta.get_fields()]:
|
||||
raise KeyError(f"{rel} is not a defined field on the {db_obj._meta.model_name} model.")
|
||||
|
||||
# if attr rel in db_obj is not None, then test if reference object(s) exist
|
||||
if getattr(db_obj, rel) is not None:
|
||||
field = db_obj._meta.get_field(rel)
|
||||
if isinstance(field, OneToOneField):
|
||||
# if the rel field is a OneToOne field, then we have already
|
||||
# determined that the object exists (is not None)
|
||||
return True
|
||||
elif isinstance(field, 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(db_obj, rel).count() > threshold
|
||||
return False
|
||||
|
||||
def _to_database(
|
||||
self,
|
||||
obj: DomainApplication,
|
||||
join: str,
|
||||
reverse_joins: list,
|
||||
should_delete: Callable,
|
||||
pre_update: Callable,
|
||||
pre_create: Callable,
|
||||
|
@ -114,26 +144,39 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
|
||||
query = getattr(obj, join).order_by("created_at").all() # order matters
|
||||
|
||||
# get the related name for the join defined for the db_obj for this form.
|
||||
# the related name will be the reference on a related object back to db_obj
|
||||
related_name = ""
|
||||
field = obj._meta.get_field(join)
|
||||
if isinstance(field, ForeignObjectRel) and callable(field.related_query_name):
|
||||
related_name = field.related_query_name()
|
||||
elif hasattr(field, "related_query_name") and callable(field.related_query_name):
|
||||
related_name = field.related_query_name()
|
||||
|
||||
# the use of `zip` pairs the forms in the formset with the
|
||||
# related objects gotten from the database -- there should always be
|
||||
# at least as many forms as database entries: extra forms means new
|
||||
# entries, but fewer forms is _not_ the correct way to delete items
|
||||
# (likely a client-side error or an attempt at data tampering)
|
||||
|
||||
for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None):
|
||||
cleaned = post_data.cleaned_data if post_data is not None else {}
|
||||
|
||||
# matching database object exists, update it
|
||||
if db_obj is not None and cleaned:
|
||||
if should_delete(cleaned):
|
||||
if any(self.has_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins):
|
||||
# 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()
|
||||
continue
|
||||
else:
|
||||
pre_update(db_obj, cleaned)
|
||||
db_obj.save()
|
||||
|
||||
# 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)
|
||||
getattr(obj, join).create(**kwargs)
|
||||
|
||||
|
@ -169,7 +212,7 @@ class TribalGovernmentForm(RegistrarForm):
|
|||
)
|
||||
|
||||
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."},
|
||||
)
|
||||
|
||||
|
@ -261,7 +304,7 @@ class OrganizationContactForm(RegistrarForm):
|
|||
validators=[
|
||||
RegexValidator(
|
||||
"^[0-9]{5}(?:-[0-9]{4})?$|^$",
|
||||
message="Enter a zip code in the required format, like 12345 or 12345-6789.",
|
||||
message="Enter a zip code in the form of 12345 or 12345-6789.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -352,7 +395,7 @@ class CurrentSitesForm(RegistrarForm):
|
|||
required=False,
|
||||
label="Public website",
|
||||
error_messages={
|
||||
"invalid": ("Enter your organization's current website in the required format, like www.city.com.")
|
||||
"invalid": ("Enter your organization's current website in the required format, like example.com.")
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -365,7 +408,9 @@ class BaseCurrentSitesFormSet(RegistrarFormSet):
|
|||
return website.strip() == ""
|
||||
|
||||
def to_database(self, obj: DomainApplication):
|
||||
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
|
||||
# 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)
|
||||
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
|
@ -417,7 +462,9 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
|
|||
return {}
|
||||
|
||||
def to_database(self, obj: DomainApplication):
|
||||
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
|
||||
# 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)
|
||||
|
||||
@classmethod
|
||||
def on_fetch(cls, query):
|
||||
|
@ -488,7 +535,7 @@ class PurposeForm(RegistrarForm):
|
|||
message="Response must be less than 1000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": "Describe how you'll use the .gov domain you’re requesting."},
|
||||
error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
|
||||
)
|
||||
|
||||
|
||||
|
@ -534,7 +581,31 @@ class YourContactForm(RegistrarForm):
|
|||
)
|
||||
phone = PhoneNumberField(
|
||||
label="Phone",
|
||||
error_messages={"required": "Enter your phone number."},
|
||||
error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."},
|
||||
)
|
||||
|
||||
|
||||
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. (We’ll ask you to explain why.)")),
|
||||
initial=initial_value,
|
||||
widget=forms.RadioSelect,
|
||||
error_messages={
|
||||
"required": "This question is required.",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
@ -561,32 +632,187 @@ class OtherContactsForm(RegistrarForm):
|
|||
)
|
||||
email = forms.EmailField(
|
||||
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(
|
||||
label="Phone",
|
||||
error_messages={"required": "Enter a phone number for this contact."},
|
||||
error_messages={
|
||||
"invalid": "Enter a valid 10-digit phone number.",
|
||||
"required": "Enter a phone number for this contact.",
|
||||
},
|
||||
)
|
||||
|
||||
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):
|
||||
"""
|
||||
This method overrides the default behavior for forms.
|
||||
This cleans the form after field validation has already taken place.
|
||||
In this override, allow for a form which is empty to be considered
|
||||
valid even though certain required fields have not passed field
|
||||
validation
|
||||
In this override, allow for a form which is deleted by user or marked for
|
||||
deletion by formset to be considered valid even though certain required fields have
|
||||
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; this is configured with REVERSE_JOINS, which is an array of
|
||||
strings representing the relationships between contact model and other models.
|
||||
"""
|
||||
|
||||
# Set form_is_empty to True initially
|
||||
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
|
||||
JOIN = "other_contacts"
|
||||
REVERSE_JOINS = [
|
||||
"user",
|
||||
"authorizing_official",
|
||||
"submitted_applications",
|
||||
"contact_applications",
|
||||
"information_authorizing_official",
|
||||
"submitted_applications_information",
|
||||
"contact_applications_information",
|
||||
]
|
||||
|
||||
if form_is_empty:
|
||||
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.REVERSE_JOINS, 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.
|
||||
"""
|
||||
|
||||
if self.form_data_marked_for_deletion:
|
||||
# clear any errors raised by the form fields
|
||||
# (before this clean() method is run, each field
|
||||
# performs its own clean, which could result in
|
||||
|
@ -600,46 +826,22 @@ class OtherContactsForm(RegistrarForm):
|
|||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class BaseOtherContactsFormSet(RegistrarFormSet):
|
||||
JOIN = "other_contacts"
|
||||
|
||||
def should_delete(self, cleaned):
|
||||
empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values())
|
||||
return all(empty)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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.",
|
||||
)
|
||||
],
|
||||
)
|
||||
def to_database(self, obj):
|
||||
"""
|
||||
This method overrides the behavior of RegistrarForm.
|
||||
If form data is marked for deletion, set relevant fields
|
||||
to None before saving.
|
||||
Do nothing if form is not valid.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
return
|
||||
if self.form_data_marked_for_deletion:
|
||||
for field_name, _ in self.fields.items():
|
||||
setattr(obj, field_name, None)
|
||||
else:
|
||||
for name, value in self.cleaned_data.items():
|
||||
setattr(obj, name, value)
|
||||
obj.save()
|
||||
|
||||
|
||||
class AnythingElseForm(RegistrarForm):
|
||||
|
|
|
@ -59,7 +59,7 @@ class DomainNameserverForm(forms.Form):
|
|||
# add custom error messages
|
||||
self.fields["server"].error_messages.update(
|
||||
{
|
||||
"required": "A minimum of 2 name servers are required.",
|
||||
"required": "At least two name servers are required.",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
262
src/registrar/management/commands/patch_federal_agency_info.py
Normal file
262
src/registrar/management/commands/patch_federal_agency_info.py
Normal 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}
|
||||
"""
|
||||
)
|
68
src/registrar/management/commands/populate_first_ready.py
Normal file
68
src/registrar/management/commands/populate_first_ready.py
Normal 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}")
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import sys
|
||||
from django.core.paginator import Paginator
|
||||
from typing import List
|
||||
from registrar.utility.enums import LogCode
|
||||
|
||||
|
@ -23,7 +24,94 @@ class TerminalColors:
|
|||
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:
|
||||
@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
|
||||
def query_yes_no(question: str, default="yes"):
|
||||
"""Ask a yes/no question via raw_input() and return their answer.
|
||||
|
|
17
src/registrar/migrations/0062_alter_host_name.py
Normal file
17
src/registrar/migrations/0062_alter_host_name.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -653,13 +653,11 @@ class DomainApplication(TimeStampedModel):
|
|||
def in_review(self):
|
||||
"""Investigate an application that has been submitted.
|
||||
|
||||
As a side effect, an email notification is sent."""
|
||||
|
||||
self._send_status_update_email(
|
||||
"application in review",
|
||||
"emails/status_change_in_review.txt",
|
||||
"emails/status_change_in_review_subject.txt",
|
||||
)
|
||||
This action is logged."""
|
||||
literal = DomainApplication.ApplicationStatus.IN_REVIEW
|
||||
# Check if the tuple exists, then grab its value
|
||||
in_review = literal if literal is not None else "In Review"
|
||||
logger.info(f"A status change occurred. {self} was changed to '{in_review}'")
|
||||
|
||||
@transition(
|
||||
field="status",
|
||||
|
@ -674,13 +672,11 @@ class DomainApplication(TimeStampedModel):
|
|||
def action_needed(self):
|
||||
"""Send back an application that is under investigation or rejected.
|
||||
|
||||
As a side effect, an email notification is sent."""
|
||||
|
||||
self._send_status_update_email(
|
||||
"action needed",
|
||||
"emails/status_change_action_needed.txt",
|
||||
"emails/status_change_action_needed_subject.txt",
|
||||
)
|
||||
This action is logged."""
|
||||
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
||||
# Check if the tuple is setup correctly, then grab its value
|
||||
action_needed = literal if literal is not None else "Action Needed"
|
||||
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
|
||||
|
||||
@transition(
|
||||
field="status",
|
||||
|
@ -840,9 +836,13 @@ class DomainApplication(TimeStampedModel):
|
|||
DomainApplication.OrganizationChoices.INTERSTATE,
|
||||
]
|
||||
|
||||
def show_no_other_contacts_rationale(self) -> bool:
|
||||
"""Show this step if the other contacts are blank."""
|
||||
return not self.other_contacts.exists()
|
||||
def has_rationale(self) -> bool:
|
||||
"""Does this application have no_other_contacts_rationale?"""
|
||||
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]:
|
||||
"""Is this application for a federal agency?
|
||||
|
|
|
@ -20,7 +20,7 @@ class Host(TimeStampedModel):
|
|||
null=False,
|
||||
blank=False,
|
||||
default=None, # prevent saving without a value
|
||||
unique=True,
|
||||
unique=False,
|
||||
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`
|
||||
help_text="Domain to which this host belongs",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.domain.name} {self.name}"
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
<div class="grid-row grow-gap">
|
||||
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
|
||||
<h1>
|
||||
{% translate "You do not have the right permissions to view this page." %}
|
||||
{% translate "You're not authorized to view this page." %}
|
||||
</h1>
|
||||
<h2>
|
||||
{% translate "Status 403" %}
|
||||
{% translate "403 error" %}
|
||||
</h2>
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
|||
{% endif %}
|
||||
<p>
|
||||
You must be an authorized user and need to be signed in to view this page.
|
||||
<a href="{% url 'login' %}"> Try logging in again</a>.
|
||||
<a href="{% url 'login' %}"> Try signing in again</a>.
|
||||
</p>
|
||||
<p>
|
||||
If you'd like help with this error <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">contact us</a>.
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
|
||||
<h1>
|
||||
{% translate "We're having some trouble" %}
|
||||
{% translate "We're having some trouble." %}
|
||||
</h1>
|
||||
<h2>
|
||||
{% translate "Status 500 – server error" %}
|
||||
{% translate "500 error" %}
|
||||
</h2>
|
||||
{% if friendly_message %}
|
||||
<p>{{ friendly_message }}</p>
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>We’d 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">
|
||||
<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>Include links to authorizing legislation, applicable bylaws or charter, or other documentation to support your claims.</li>
|
||||
<li>How your organization operates independently from a state government</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>
|
||||
</p>
|
||||
<h2>What can you tell us about your organization?</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{% include "includes/ao_example.html" %}
|
||||
</div>
|
||||
|
||||
<p>We typically don’t 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 don’t reach out to the authorizing official, but if contact is necessary, our practice is to coordinate with you, the requestor, first.</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
{% load static field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>Enter your organization’s current public website, if you have one. For example,
|
||||
www.city.com. We can better evaluate your domain request if we know about domains
|
||||
you’re already using. If you already have any .gov domains please include them. This question is optional.</p>
|
||||
<p>We can better evaluate your request if we know about domains you’re already using.</p>
|
||||
<h2>What are the current websites for your organization?</h2>
|
||||
<p>Enter your organization’s current public websites. If you already have a .gov domain, include that in your list. This question is optional.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
|
|
|
@ -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 %}
|
|
@ -2,15 +2,12 @@
|
|||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<h2 class="margin-bottom-05">
|
||||
What is the name and mailing address of your organization?
|
||||
</h2>
|
||||
<p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="https://beta.get.gov/about/data/" target="_blank">.gov’s public data.</a></p>
|
||||
|
||||
<p>Enter the name of the organization you represent. Your organization might be part
|
||||
of a larger entity. If so, enter information about your part of the larger entity.</p>
|
||||
<h2>What is the name and mailing address of the organization you represent?</h2>
|
||||
|
||||
<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 %}
|
||||
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
{% load field_helpers %}
|
||||
|
||||
{% 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>
|
||||
|
||||
|
|
|
@ -13,19 +13,47 @@
|
|||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block form_fields %}
|
||||
{{ forms.0.management_form }}
|
||||
{# forms.0 is a formset and this iterates over its forms #}
|
||||
{% for form in forms.0.forms %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Organization contact {{ forloop.counter }} (optional)</h2>
|
||||
<h2>Are there other employees who can help verify your request?</h2>
|
||||
</legend>
|
||||
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.has_other_contacts %}
|
||||
{% endwith %}
|
||||
{# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #}
|
||||
|
||||
</fieldset>
|
||||
|
||||
<div id="other-employees" class="other-contacts-form">
|
||||
{% include "includes/required_fields.html" %}
|
||||
{{ forms.1.management_form }}
|
||||
{# forms.1 is a formset and this iterates over its forms #}
|
||||
{% 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 %}
|
||||
|
||||
|
@ -33,7 +61,13 @@
|
|||
|
||||
{% 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 %}
|
||||
|
@ -42,9 +76,23 @@
|
|||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
||||
<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 don't 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 %}
|
||||
|
|
|
@ -99,15 +99,15 @@
|
|||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% for other in application.other_contacts.all %}
|
||||
<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 %}
|
||||
</div>
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if step == Step.NO_OTHER_CONTACTS %}
|
||||
<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 %}
|
||||
{% endif %}
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{{ application.anything_else|default:"No" }}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% 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 %}
|
||||
|
||||
{% with sublabel_text="Please include the entire name of your tribe as recognized by the Bureau of Indian Affairs." %}
|
||||
{% with link_text="Bureau of Indian Affairs" %}
|
||||
{% 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" %}
|
||||
<h2>What is the name of the tribe you represent?</h2>
|
||||
<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 external_link="true" target_blank="true" %}
|
||||
{% input_with_errors forms.0.tribe_name %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend class="usa-legend">
|
||||
<p>Is your organization a federally-recognized tribe or a state-recognized tribe? Check all that apply.
|
||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
|
||||
<h2>Is your organization a federally-recognized tribe or a state-recognized tribe?</h2>
|
||||
</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.state_recognized_tribe %}
|
||||
</fieldset>
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</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
|
||||
{% 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
|
||||
{% else %}
|
||||
{{ domain.state|title }}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
{% 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 %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
|
@ -114,7 +114,7 @@
|
|||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||
data-force-action
|
||||
>
|
||||
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to removing your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
|
||||
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
{% 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 %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
|
|
|
@ -3,30 +3,38 @@ Hi.
|
|||
|
||||
{{ 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’ll 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
|
||||
account. If you don’t already have one, follow these steps to create your
|
||||
a simple and secure process for signing in to many government services with one
|
||||
account.
|
||||
|
||||
If you don’t already have one, follow these steps to create your
|
||||
Login.gov account <https://login.gov/help/get-started/create-your-account/>.
|
||||
|
||||
|
||||
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.
|
||||
You’ll 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?
|
||||
If you’re 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
|
||||
|
||||
.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
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Visit <https://get.gov>
|
||||
Learn about .gov <https://get.gov>
|
||||
{% endautoescape %}
|
|
@ -1,26 +1,28 @@
|
|||
{% 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 }}
|
||||
REQUEST #: {{ application.id }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
STATUS: Withdrawn
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
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 didn’t 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
|
||||
|
||||
.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>
|
||||
Learn about .gov <https://get.gov>
|
||||
{% endautoescape %}
|
|
@ -1 +1 @@
|
|||
Your .gov domain request has been withdrawn
|
||||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
|
|
|
@ -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 %}
|
|
@ -1 +0,0 @@
|
|||
Action needed for your .gov domain request
|
|
@ -1,40 +1,43 @@
|
|||
{% 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.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
REQUEST #: {{ application.id }}
|
||||
STATUS: In review
|
||||
STATUS: Approved
|
||||
|
||||
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, you’ll first need to connect it to a Domain Name System (DNS) hosting service. At this time, we don’t 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 don’t provide DNS hosting services.
|
||||
Go to the domain management page to add your domain name server information <https://manage.get.gov/domain/{{ application.approved_domain.id }}/nameservers>.
|
||||
After you’ve set up hosting, you’ll need to enter your name server information on the .gov registrar.
|
||||
|
||||
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
|
||||
Currently, you’re 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>.
|
||||
|
||||
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.
|
||||
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/>
|
||||
|
||||
|
||||
THANK YOU
|
||||
|
||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Visit <https://get.gov>
|
||||
Learn about .gov <https://get.gov>
|
||||
{% endautoescape %}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Your .gov domain request is approved
|
||||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
|
|
|
@ -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
|
||||
|
||||
- We’re 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 }}>
|
||||
|
||||
- We’ll 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 %}
|
|
@ -1 +0,0 @@
|
|||
Your .gov domain request is being reviewed
|
|
@ -1,32 +1,32 @@
|
|||
{% 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.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
REQUEST #: {{ application.id }}
|
||||
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
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Visit <https://get.gov>
|
||||
Learn about .gov <https://get.gov>
|
||||
{% endautoescape %}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Your .gov domain request has been rejected
|
||||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
|
|
|
@ -1,35 +1,31 @@
|
|||
{% 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.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
REQUEST #: {{ application.id }}
|
||||
STATUS: Received
|
||||
STATUS: Submitted
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
NEXT STEPS
|
||||
We’ll review your request. This usually takes 20 business days. During this review we’ll 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
|
||||
|
||||
We’ll 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?
|
||||
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
|
||||
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
|
||||
|
||||
- We’ll 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 }}>
|
||||
|
||||
- We’ll email you with questions or when we complete our review.
|
||||
Learn more about withdrawing your request <https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
|
||||
|
||||
|
||||
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
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Visit <https://get.gov>
|
||||
Learn about .gov <https://get.gov>
|
||||
{% endautoescape %}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Thank you for applying for a .gov domain
|
||||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
|
|
|
@ -52,9 +52,10 @@
|
|||
</th>
|
||||
<td data-sort-value="{{ domain.expiration_date|date:"U" }}" data-label="Expires">{{ domain.expiration_date|date }}</td>
|
||||
<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
|
||||
{% elif domain.state == "unknown" or domain.state == "dns needed"%}
|
||||
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
||||
DNS needed
|
||||
{% else %}
|
||||
{{ domain.state|title }}
|
||||
|
|
|
@ -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(
|
||||
"fakepw",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
|
@ -1058,6 +1077,7 @@ class MockEppLib(TestCase):
|
|||
"freeman.gov": (self.InfoDomainWithContacts, None),
|
||||
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
|
||||
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
|
||||
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
|
||||
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
|
||||
"justnameserver.com": (self.justNameserver, None),
|
||||
}
|
||||
|
@ -1087,6 +1107,8 @@ class MockEppLib(TestCase):
|
|||
mocked_result = self.mockDefaultSecurityContact
|
||||
case "defaultTech":
|
||||
mocked_result = self.mockDefaultTechnicalContact
|
||||
case "defaultVeri":
|
||||
mocked_result = self.mockVerisignDataInfoContact
|
||||
case _:
|
||||
# Default contact return
|
||||
mocked_result = self.mockDataInfoContact
|
||||
|
|
|
@ -457,44 +457,6 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
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
|
||||
def test_save_model_sends_approved_email(self):
|
||||
# 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
|
||||
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
|
||||
def test_save_model_sends_rejected_email(self):
|
||||
# make sure there is no user with this email
|
||||
|
|
|
@ -34,7 +34,7 @@ class TestFormValidation(MockEppLib):
|
|||
form = OrganizationContactForm(data={"zipcode": "nah"})
|
||||
self.assertEqual(
|
||||
form.errors["zipcode"],
|
||||
["Enter a zip code in the required format, like 12345 or 12345-6789."],
|
||||
["Enter a zip code in the form of 12345 or 12345-6789."],
|
||||
)
|
||||
|
||||
def test_org_contact_zip_valid(self):
|
||||
|
@ -46,7 +46,7 @@ class TestFormValidation(MockEppLib):
|
|||
form = CurrentSitesForm(data={"website": "nah"})
|
||||
self.assertEqual(
|
||||
form.errors["website"],
|
||||
["Enter your organization's current website in the required format, like www.city.com."],
|
||||
["Enter your organization's current website in the required format, like example.com."],
|
||||
)
|
||||
|
||||
def test_website_valid(self):
|
||||
|
@ -318,7 +318,7 @@ class TestFormValidation(MockEppLib):
|
|||
def test_your_contact_phone_invalid(self):
|
||||
"""Must be a valid phone number."""
|
||||
form = YourContactForm(data={"phone": "boss@boss"})
|
||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number "))
|
||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
|
||||
|
||||
def test_other_contact_email_invalid(self):
|
||||
"""must be a valid email address."""
|
||||
|
@ -331,7 +331,7 @@ class TestFormValidation(MockEppLib):
|
|||
def test_other_contact_phone_invalid(self):
|
||||
"""Must be a valid phone number."""
|
||||
form = OtherContactsForm(data={"phone": "super@boss"})
|
||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number "))
|
||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
|
||||
|
||||
def test_requirements_form_blank(self):
|
||||
"""Requirements box unchecked is an error."""
|
||||
|
|
443
src/registrar/tests/test_management_scripts.py
Normal file
443
src/registrar/tests/test_management_scripts.py
Normal file
|
@ -0,0 +1,443 @@
|
|||
import copy
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from registrar.models import (
|
||||
User,
|
||||
Domain,
|
||||
DomainInvitation,
|
||||
TransitionDomain,
|
||||
DomainInformation,
|
||||
UserDomainRole,
|
||||
)
|
||||
|
||||
from django.core.management import call_command
|
||||
from unittest.mock import patch
|
||||
|
||||
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))
|
|
@ -268,8 +268,6 @@ class TestDomainApplication(TestCase):
|
|||
(self.ineligible_application, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for application, exception_type in test_cases:
|
||||
with self.subTest(application=application, exception_type=exception_type):
|
||||
try:
|
||||
|
@ -288,8 +286,6 @@ class TestDomainApplication(TestCase):
|
|||
(self.withdrawn_application, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for application, exception_type in test_cases:
|
||||
with self.subTest(application=application, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
|
@ -500,6 +496,28 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
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):
|
||||
"""Test the User-Domain-Role connection."""
|
||||
|
|
|
@ -10,7 +10,7 @@ class TestNameserverError(TestCase):
|
|||
def test_with_no_ip(self):
|
||||
"""Test NameserverError when no ip address is passed"""
|
||||
nameserver = "nameserver val"
|
||||
expected = "Using your domain for a name server requires an IP address"
|
||||
expected = "Using your domain for a name server requires an IP address."
|
||||
|
||||
nsException = NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
|
||||
self.assertEqual(nsException.message, expected)
|
||||
|
@ -20,7 +20,7 @@ class TestNameserverError(TestCase):
|
|||
"""Test NameserverError when no ip address
|
||||
and no nameserver is passed"""
|
||||
nameserver = "nameserver val"
|
||||
expected = "Too many hosts provided, you may not have more than 13 nameservers."
|
||||
expected = "You can't have more than 13 nameservers."
|
||||
|
||||
nsException = NameserverError(code=nsErrorCodes.TOO_MANY_HOSTS, nameserver=nameserver)
|
||||
self.assertEqual(nsException.message, expected)
|
||||
|
|
|
@ -4,8 +4,10 @@ from django.test import Client, RequestFactory, TestCase
|
|||
from io import StringIO
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.models.user import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from registrar.tests.common import MockEppLib
|
||||
from registrar.utility.csv_export import (
|
||||
write_header,
|
||||
write_body,
|
||||
|
@ -221,8 +223,9 @@ class CsvReportsTest(TestCase):
|
|||
self.assertEqual(expected_file_content, response.content)
|
||||
|
||||
|
||||
class ExportDataTest(TestCase):
|
||||
class ExportDataTest(MockEppLib):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
|
@ -327,11 +330,85 @@ class ExportDataTest(TestCase):
|
|||
)
|
||||
|
||||
def tearDown(self):
|
||||
PublicContact.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
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,dotgov@cisa.dhs.gov,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):
|
||||
"""Test that write_body returns the
|
||||
existing domain, test that sort by domain name works,
|
||||
|
|
|
@ -18,178 +18,10 @@ from unittest.mock import patch
|
|||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""This test case class is designed to verify the idempotency of migrations
|
||||
related to domain transitions in the application."""
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.conf import settings
|
|||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore
|
||||
from django_webtest import WebTest # type: ignore
|
||||
import boto3_mocking # type: ignore
|
||||
|
@ -35,8 +36,13 @@ from registrar.models import (
|
|||
User,
|
||||
)
|
||||
from registrar.views.application import ApplicationWizard, Step
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from .common import less_console_noise
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestViews(TestCase):
|
||||
|
@ -54,9 +60,9 @@ class TestViews(TestCase):
|
|||
|
||||
def test_application_form_not_logged_in(self):
|
||||
"""Application form not accessible without a logged-in user."""
|
||||
response = self.client.get("/register/")
|
||||
response = self.client.get("/request/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/login?next=/register/", response.headers["Location"])
|
||||
self.assertIn("/login?next=/request/", response.headers["Location"])
|
||||
|
||||
|
||||
class TestWithUser(MockEppLib):
|
||||
|
@ -74,6 +80,7 @@ class TestWithUser(MockEppLib):
|
|||
# delete any applications too
|
||||
super().tearDown()
|
||||
DomainApplication.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
self.user.delete()
|
||||
|
||||
|
||||
|
@ -93,20 +100,8 @@ class LoggedInTests(TestWithUser):
|
|||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_lists_domains(self):
|
||||
response = self.client.get("/")
|
||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
role, _ = UserDomainRole.objects.get_or_create(user=self.user, domain=domain, role=UserDomainRole.Roles.MANAGER)
|
||||
response = self.client.get("/")
|
||||
# count = 2 because it is also in screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=2)
|
||||
self.assertContains(response, "Expired")
|
||||
# clean up
|
||||
role.delete()
|
||||
|
||||
def test_application_form_view(self):
|
||||
response = self.client.get("/register/", follow=True)
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertContains(
|
||||
response,
|
||||
"You’re about to start your .gov domain request.",
|
||||
|
@ -120,7 +115,7 @@ class LoggedInTests(TestWithUser):
|
|||
self.user.save()
|
||||
|
||||
with less_console_noise():
|
||||
response = self.client.get("/register/", follow=True)
|
||||
response = self.client.get("/request/", follow=True)
|
||||
print(response.status_code)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
@ -154,7 +149,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
self.assertEqual(detail_page.status_code, 302)
|
||||
# You can access the 'Location' header to get the redirect URL
|
||||
redirect_url = detail_page.url
|
||||
self.assertEqual(redirect_url, "/register/organization_type/")
|
||||
self.assertEqual(redirect_url, "/request/organization_type/")
|
||||
|
||||
def test_application_form_empty_submit(self):
|
||||
"""Tests empty submit on the first page after the acknowledgement page"""
|
||||
|
@ -216,8 +211,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
in the modal header on the submit page.
|
||||
"""
|
||||
num_pages_tested = 0
|
||||
# elections, type_of_work, tribal_government, no_other_contacts
|
||||
SKIPPED_PAGES = 4
|
||||
# elections, type_of_work, tribal_government
|
||||
SKIPPED_PAGES = 3
|
||||
num_pages = len(self.TITLES) - SKIPPED_PAGES
|
||||
|
||||
intro_page = self.app.get(reverse("application:"))
|
||||
|
@ -248,7 +243,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(type_result.status_code, 302)
|
||||
self.assertEqual(type_result["Location"], "/register/organization_federal/")
|
||||
self.assertEqual(type_result["Location"], "/request/organization_federal/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- FEDERAL BRANCH PAGE ----
|
||||
|
@ -268,7 +263,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(federal_result.status_code, 302)
|
||||
self.assertEqual(federal_result["Location"], "/register/organization_contact/")
|
||||
self.assertEqual(federal_result["Location"], "/request/organization_contact/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- ORG CONTACT PAGE ----
|
||||
|
@ -301,7 +296,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(org_contact_result.status_code, 302)
|
||||
self.assertEqual(org_contact_result["Location"], "/register/authorizing_official/")
|
||||
self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- AUTHORIZING OFFICIAL PAGE ----
|
||||
|
@ -326,7 +321,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(ao_result.status_code, 302)
|
||||
self.assertEqual(ao_result["Location"], "/register/current_sites/")
|
||||
self.assertEqual(ao_result["Location"], "/request/current_sites/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- CURRENT SITES PAGE ----
|
||||
|
@ -348,7 +343,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(current_sites_result.status_code, 302)
|
||||
self.assertEqual(current_sites_result["Location"], "/register/dotgov_domain/")
|
||||
self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- DOTGOV DOMAIN PAGE ----
|
||||
|
@ -368,7 +363,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(dotgov_result.status_code, 302)
|
||||
self.assertEqual(dotgov_result["Location"], "/register/purpose/")
|
||||
self.assertEqual(dotgov_result["Location"], "/request/purpose/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- PURPOSE PAGE ----
|
||||
|
@ -387,7 +382,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(purpose_result.status_code, 302)
|
||||
self.assertEqual(purpose_result["Location"], "/register/your_contact/")
|
||||
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- YOUR CONTACT INFO PAGE ----
|
||||
|
@ -415,15 +410,20 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(your_contact_result.status_code, 302)
|
||||
self.assertEqual(your_contact_result["Location"], "/register/other_contacts/")
|
||||
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- OTHER CONTACTS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
other_contacts_page = your_contact_result.follow()
|
||||
|
||||
# This page has 3 forms in 1.
|
||||
# Let's set the yes/no radios to enable the other contacts fieldsets
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "True"
|
||||
|
||||
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
|
||||
other_contacts_form["other_contacts-0-last_name"] = "Tester2"
|
||||
other_contacts_form["other_contacts-0-title"] = "Another Tester"
|
||||
|
@ -448,7 +448,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(other_contacts_result.status_code, 302)
|
||||
self.assertEqual(other_contacts_result["Location"], "/register/anything_else/")
|
||||
self.assertEqual(other_contacts_result["Location"], "/request/anything_else/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- ANYTHING ELSE PAGE ----
|
||||
|
@ -468,7 +468,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(anything_else_result.status_code, 302)
|
||||
self.assertEqual(anything_else_result["Location"], "/register/requirements/")
|
||||
self.assertEqual(anything_else_result["Location"], "/request/requirements/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- REQUIREMENTS PAGE ----
|
||||
|
@ -488,7 +488,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the application
|
||||
self.assertEqual(requirements_result.status_code, 302)
|
||||
self.assertEqual(requirements_result["Location"], "/register/review/")
|
||||
self.assertEqual(requirements_result["Location"], "/request/review/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- REVIEW AND FINSIHED PAGES ----
|
||||
|
@ -542,7 +542,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
review_result = review_form.submit()
|
||||
|
||||
self.assertEqual(review_result.status_code, 302)
|
||||
self.assertEqual(review_result["Location"], "/register/finished/")
|
||||
self.assertEqual(review_result["Location"], "/request/finished/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# following this redirect is a GET request, so include the cookie
|
||||
|
@ -561,8 +561,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
@skip("WIP")
|
||||
def test_application_form_started_allsteps(self):
|
||||
num_pages_tested = 0
|
||||
# elections, type_of_work, tribal_government, no_other_contacts
|
||||
SKIPPED_PAGES = 4
|
||||
# elections, type_of_work, tribal_government
|
||||
SKIPPED_PAGES = 3
|
||||
DASHBOARD_PAGE = 1
|
||||
num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE
|
||||
|
||||
|
@ -623,7 +623,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the federal branch
|
||||
# question
|
||||
self.assertEqual(type_result.status_code, 302)
|
||||
self.assertEqual(type_result["Location"], "/register/organization_federal/")
|
||||
self.assertEqual(type_result["Location"], "/request/organization_federal/")
|
||||
|
||||
# and the step label should appear in the sidebar of the resulting page
|
||||
# but the step label for the elections page should not appear
|
||||
|
@ -640,7 +640,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the contact
|
||||
# question
|
||||
self.assertEqual(federal_result.status_code, 302)
|
||||
self.assertEqual(federal_result["Location"], "/register/organization_contact/")
|
||||
self.assertEqual(federal_result["Location"], "/request/organization_contact/")
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
contact_page = federal_result.follow()
|
||||
self.assertContains(contact_page, "Federal agency")
|
||||
|
@ -677,7 +677,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
# the post request should return a redirect to the elections question
|
||||
self.assertEqual(type_result.status_code, 302)
|
||||
self.assertEqual(type_result["Location"], "/register/organization_election/")
|
||||
self.assertEqual(type_result["Location"], "/request/organization_election/")
|
||||
|
||||
# and the step label should appear in the sidebar of the resulting page
|
||||
# but the step label for the elections page should not appear
|
||||
|
@ -694,7 +694,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the contact
|
||||
# question
|
||||
self.assertEqual(election_result.status_code, 302)
|
||||
self.assertEqual(election_result["Location"], "/register/organization_contact/")
|
||||
self.assertEqual(election_result["Location"], "/request/organization_contact/")
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
contact_page = election_result.follow()
|
||||
self.assertNotContains(contact_page, "Federal agency")
|
||||
|
@ -732,7 +732,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
# Should be a link to the organization_federal page
|
||||
self.assertGreater(
|
||||
len(new_page.html.find_all("a", href="/register/organization_federal/")),
|
||||
len(new_page.html.find_all("a", href="/request/organization_federal/")),
|
||||
0,
|
||||
)
|
||||
|
||||
|
@ -779,7 +779,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the
|
||||
# about your organization page if it was successful.
|
||||
self.assertEqual(contact_result.status_code, 302)
|
||||
self.assertEqual(contact_result["Location"], "/register/about_your_organization/")
|
||||
self.assertEqual(contact_result["Location"], "/request/about_your_organization/")
|
||||
|
||||
def test_application_about_your_organization_special(self):
|
||||
"""Special districts have to answer an additional question."""
|
||||
|
@ -809,28 +809,151 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
|
||||
|
||||
def test_application_no_other_contacts(self):
|
||||
"""Applicants with no other contacts have to give a reason."""
|
||||
contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
def test_yes_no_form_inits_blank_for_new_application(self):
|
||||
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
||||
new applications"""
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
|
||||
|
||||
def test_yes_no_form_inits_yes_for_application_with_other_contacts(self):
|
||||
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
|
||||
application has other contacts"""
|
||||
# Application has other contacts by default
|
||||
application = completed_application(user=self.user)
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
|
||||
|
||||
def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self):
|
||||
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
|
||||
application has no other contacts"""
|
||||
# Application has other contacts by default
|
||||
application = completed_application(user=self.user, has_other_contacts=False)
|
||||
application.no_other_contacts_rationale = "Hello!"
|
||||
application.save()
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
|
||||
|
||||
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
|
||||
"""When a user submits the Other Contacts form with other contacts selected, the application's
|
||||
no other contacts rationale gets deleted"""
|
||||
# Application has other contacts by default
|
||||
application = completed_application(user=self.user, has_other_contacts=False)
|
||||
application.no_other_contacts_rationale = "Hello!"
|
||||
application.save()
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
|
||||
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "True"
|
||||
|
||||
other_contacts_form["other_contacts-0-first_name"] = "Testy"
|
||||
other_contacts_form["other_contacts-0-middle_name"] = ""
|
||||
other_contacts_form["other_contacts-0-last_name"] = "McTesterson"
|
||||
other_contacts_form["other_contacts-0-title"] = "Lord"
|
||||
other_contacts_form["other_contacts-0-email"] = "testy@abc.org"
|
||||
other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123"
|
||||
|
||||
# Submit the now empty form
|
||||
other_contacts_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
result = contacts_page.forms[0].submit()
|
||||
# follow first redirect
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
no_contacts_page = result.follow()
|
||||
expected_url_slug = str(Step.NO_OTHER_CONTACTS)
|
||||
actual_url_slug = no_contacts_page.request.path.split("/")[-2]
|
||||
self.assertEqual(expected_url_slug, actual_url_slug)
|
||||
|
||||
def test_application_delete_other_contact(self):
|
||||
"""Other contacts can be deleted after being saved to database."""
|
||||
# Populate the databse with a domain application that
|
||||
# Verify that the no_other_contacts_rationale we saved earlier has been removed from the database
|
||||
application = DomainApplication.objects.get()
|
||||
self.assertEqual(
|
||||
application.other_contacts.count(),
|
||||
1,
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
application.no_other_contacts_rationale,
|
||||
None,
|
||||
)
|
||||
|
||||
def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self):
|
||||
"""When a user submits the Other Contacts form with no other contacts selected, the application's
|
||||
other contacts get deleted for other contacts that exist and are not joined to other objects
|
||||
"""
|
||||
# Application has other contacts by default
|
||||
application = completed_application(user=self.user)
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
|
||||
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "False"
|
||||
|
||||
other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!"
|
||||
|
||||
# Submit the now empty form
|
||||
other_contacts_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Verify that the no_other_contacts_rationale we saved earlier has been removed from the database
|
||||
application = DomainApplication.objects.get()
|
||||
self.assertEqual(
|
||||
application.other_contacts.count(),
|
||||
0,
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
application.no_other_contacts_rationale,
|
||||
"Hello again!",
|
||||
)
|
||||
|
||||
def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self):
|
||||
"""When a user submits the Other Contacts form with no other contacts selected, the application's
|
||||
other contacts references get removed for other contacts that exist and are joined to other objects"""
|
||||
# Populate the database with a domain application that
|
||||
# has 1 "other contact" assigned to it
|
||||
# We'll do it from scratch so we can reuse the other contact
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
|
@ -869,6 +992,141 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
)
|
||||
application.other_contacts.add(other)
|
||||
|
||||
# Now let's join the other contact to another object
|
||||
domain_info = DomainInformation.objects.create(creator=self.user)
|
||||
domain_info.other_contacts.set([other])
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
|
||||
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "False"
|
||||
|
||||
other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!"
|
||||
|
||||
# Submit the now empty form
|
||||
other_contacts_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application
|
||||
application = DomainApplication.objects.get()
|
||||
self.assertEqual(
|
||||
application.other_contacts.count(),
|
||||
0,
|
||||
)
|
||||
|
||||
# Verify that the 'other' contact object still exists
|
||||
domain_info = DomainInformation.objects.get()
|
||||
self.assertEqual(
|
||||
domain_info.other_contacts.count(),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(
|
||||
domain_info.other_contacts.all()[0].first_name,
|
||||
"Testy2",
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
application.no_other_contacts_rationale,
|
||||
"Hello again!",
|
||||
)
|
||||
|
||||
def test_if_yes_no_form_is_no_then_no_other_contacts_required(self):
|
||||
"""Applicants with no other contacts have to give a reason."""
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "False"
|
||||
response = other_contacts_page.forms[0].submit()
|
||||
|
||||
# The textarea for no other contacts returns this error message
|
||||
# Assert that it is returned, ie the no other contacts form is required
|
||||
self.assertContains(response, "Rationale for no other employees is required.")
|
||||
|
||||
# The first name field for other contacts returns this error message
|
||||
# Assert that it is not returned, ie the contacts form is not required
|
||||
self.assertNotContains(response, "Enter the first name / given name of this contact.")
|
||||
|
||||
def test_if_yes_no_form_is_yes_then_other_contacts_required(self):
|
||||
"""Applicants with other contacts do not have to give a reason."""
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "True"
|
||||
response = other_contacts_page.forms[0].submit()
|
||||
|
||||
# The textarea for no other contacts returns this error message
|
||||
# Assert that it is not returned, ie the no other contacts form is not required
|
||||
self.assertNotContains(response, "Rationale for no other employees is required.")
|
||||
|
||||
# The first name field for other contacts returns this error message
|
||||
# Assert that it is returned, ie the contacts form is required
|
||||
self.assertContains(response, "Enter the first name / given name of this contact.")
|
||||
|
||||
def test_delete_other_contact(self):
|
||||
"""Other contacts can be deleted after being saved to database.
|
||||
|
||||
This formset uses the DJANGO DELETE widget. We'll test that by setting 2 contacts on an application,
|
||||
loading the form and marking one contact up for deletion."""
|
||||
# Populate the database with a domain application that
|
||||
# has 2 "other contact" assigned to it
|
||||
# We'll do it from scratch so we can reuse the other contact
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(201) 555 5555",
|
||||
)
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
title="Admin Tester",
|
||||
email="testy-admin@town.com",
|
||||
phone="(201) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(201) 555 5557",
|
||||
)
|
||||
other2, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy3",
|
||||
last_name="Tester3",
|
||||
title="Another Tester",
|
||||
email="testy3@town.com",
|
||||
phone="(201) 555 5557",
|
||||
)
|
||||
application, _ = DomainApplication.objects.get_or_create(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
anything_else="No",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
address_line1="address 1",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
application.other_contacts.add(other)
|
||||
application.other_contacts.add(other2)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
|
@ -883,35 +1141,170 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
||||
# Minimal check to ensure the form is loaded with data (if this part of
|
||||
# the application doesn't work, we should be equipped with other unit
|
||||
# tests to flag it)
|
||||
# Minimal check to ensure the form is loaded with both other contacts
|
||||
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
|
||||
self.assertEqual(other_contacts_form["other_contacts-1-first_name"].value, "Testy3")
|
||||
|
||||
# clear the form
|
||||
other_contacts_form["other_contacts-0-first_name"] = ""
|
||||
other_contacts_form["other_contacts-0-middle_name"] = ""
|
||||
other_contacts_form["other_contacts-0-last_name"] = ""
|
||||
other_contacts_form["other_contacts-0-title"] = ""
|
||||
other_contacts_form["other_contacts-0-email"] = ""
|
||||
other_contacts_form["other_contacts-0-phone"] = ""
|
||||
# Mark the first dude for deletion
|
||||
other_contacts_form.set("other_contacts-0-DELETE", "on")
|
||||
|
||||
# Submit the now empty form
|
||||
result = other_contacts_form.submit()
|
||||
# Submit the form
|
||||
other_contacts_form.submit()
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Verify that the contact we saved earlier has been removed from the database
|
||||
application = DomainApplication.objects.get() # There are no contacts anymore
|
||||
self.assertEqual(
|
||||
application.other_contacts.count(),
|
||||
0,
|
||||
)
|
||||
# Verify that the first dude was deleted
|
||||
application = DomainApplication.objects.get()
|
||||
self.assertEqual(application.other_contacts.count(), 1)
|
||||
self.assertEqual(application.other_contacts.first().first_name, "Testy3")
|
||||
|
||||
# Verify that on submit, user is advanced to "no contacts" page
|
||||
no_contacts_page = result.follow()
|
||||
expected_url_slug = str(Step.NO_OTHER_CONTACTS)
|
||||
actual_url_slug = no_contacts_page.request.path.split("/")[-2]
|
||||
self.assertEqual(expected_url_slug, actual_url_slug)
|
||||
def test_delete_other_contact_does_not_allow_zero_contacts(self):
|
||||
"""Delete Other Contact does not allow submission with zero contacts."""
|
||||
# Populate the database with a domain application that
|
||||
# has 1 "other contact" assigned to it
|
||||
# We'll do it from scratch so we can reuse the other contact
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(201) 555 5555",
|
||||
)
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
title="Admin Tester",
|
||||
email="testy-admin@town.com",
|
||||
phone="(201) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(201) 555 5557",
|
||||
)
|
||||
application, _ = DomainApplication.objects.get_or_create(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
anything_else="No",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
address_line1="address 1",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
application.other_contacts.add(other)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
||||
# Minimal check to ensure the form is loaded
|
||||
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
|
||||
|
||||
# Mark the first dude for deletion
|
||||
other_contacts_form.set("other_contacts-0-DELETE", "on")
|
||||
|
||||
# Submit the form
|
||||
other_contacts_form.submit()
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Verify that the contact was not deleted
|
||||
application = DomainApplication.objects.get()
|
||||
self.assertEqual(application.other_contacts.count(), 1)
|
||||
self.assertEqual(application.other_contacts.first().first_name, "Testy2")
|
||||
|
||||
def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self):
|
||||
"""When you:
|
||||
1. add an empty contact,
|
||||
2. delete existing contacts,
|
||||
3. then submit,
|
||||
The forms on page reload shows all the required fields and their errors."""
|
||||
|
||||
# Populate the database with a domain application that
|
||||
# has 1 "other contact" assigned to it
|
||||
# We'll do it from scratch so we can reuse the other contact
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(201) 555 5555",
|
||||
)
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
title="Admin Tester",
|
||||
email="testy-admin@town.com",
|
||||
phone="(201) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(201) 555 5557",
|
||||
)
|
||||
application, _ = DomainApplication.objects.get_or_create(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
anything_else="No",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
address_line1="address 1",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
application.other_contacts.add(other)
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-application", kwargs={"id": application.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_page = self.app.get(reverse("application:other_contacts"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
||||
# Minimal check to ensure the form is loaded
|
||||
self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2")
|
||||
|
||||
# Set total forms to 2 indicating an additional formset was added.
|
||||
# Submit no data though for the second formset.
|
||||
# Set the first formset to be deleted.
|
||||
other_contacts_form["other_contacts-TOTAL_FORMS"] = "2"
|
||||
other_contacts_form.set("other_contacts-0-DELETE", "on")
|
||||
|
||||
response = other_contacts_form.submit()
|
||||
|
||||
# Assert that the response presents errors to the user, including to
|
||||
# Enter the first name ...
|
||||
self.assertContains(response, "Enter the first name / given name of this contact.")
|
||||
|
||||
def test_application_about_your_organiztion_interstate(self):
|
||||
"""Special districts have to answer an additional question."""
|
||||
|
@ -1352,8 +1745,20 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov")
|
||||
self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com")
|
||||
self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov")
|
||||
self.domain_on_hold, _ = Domain.objects.get_or_create(name="on-hold.gov", state=Domain.State.ON_HOLD)
|
||||
self.domain_deleted, _ = Domain.objects.get_or_create(name="deleted.gov", state=Domain.State.DELETED)
|
||||
self.domain_on_hold, _ = Domain.objects.get_or_create(
|
||||
name="on-hold.gov",
|
||||
state=Domain.State.ON_HOLD,
|
||||
expiration_date=timezone.make_aware(
|
||||
datetime.combine(date.today() + timedelta(days=1), datetime.min.time())
|
||||
),
|
||||
)
|
||||
self.domain_deleted, _ = Domain.objects.get_or_create(
|
||||
name="deleted.gov",
|
||||
state=Domain.State.DELETED,
|
||||
expiration_date=timezone.make_aware(
|
||||
datetime.combine(date.today() + timedelta(days=1), datetime.min.time())
|
||||
),
|
||||
)
|
||||
|
||||
self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
|
||||
self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov")
|
||||
|
@ -1493,14 +1898,59 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
|||
|
||||
|
||||
class TestDomainDetail(TestDomainOverview):
|
||||
@skip("Assertion broke for no reason, why? Need to fix")
|
||||
def test_domain_detail_link_works(self):
|
||||
home_page = self.app.get("/")
|
||||
logger.info(f"This is the value of home_page: {home_page}")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
# click the "Edit" link
|
||||
detail_page = home_page.click("Manage", index=0)
|
||||
self.assertContains(detail_page, "igorville.gov")
|
||||
self.assertContains(detail_page, "Status")
|
||||
|
||||
def test_unknown_domain_does_not_show_as_expired_on_homepage(self):
|
||||
"""An UNKNOWN domain does not show as expired on the homepage.
|
||||
It shows as 'DNS needed'"""
|
||||
# At the time of this test's writing, there are 6 UNKNOWN domains inherited
|
||||
# from constructors. Let's reset.
|
||||
Domain.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
home_page = self.app.get("/")
|
||||
self.assertNotContains(home_page, "igorville.gov")
|
||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
igorville = Domain.objects.get(name="igorville.gov")
|
||||
self.assertEquals(igorville.state, Domain.State.UNKNOWN)
|
||||
self.assertNotContains(home_page, "Expired")
|
||||
self.assertContains(home_page, "DNS needed")
|
||||
|
||||
def test_unknown_domain_does_not_show_as_expired_on_detail_page(self):
|
||||
"""An UNKNOWN domain does not show as expired on the detail page.
|
||||
It shows as 'DNS needed'"""
|
||||
# At the time of this test's writing, there are 6 UNKNOWN domains inherited
|
||||
# from constructors. Let's reset.
|
||||
Domain.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
igorville = Domain.objects.get(name="igorville.gov")
|
||||
self.assertEquals(igorville.state, Domain.State.UNKNOWN)
|
||||
detail_page = home_page.click("Manage", index=0)
|
||||
self.assertNotContains(detail_page, "Expired")
|
||||
|
||||
self.assertContains(detail_page, "DNS needed")
|
||||
|
||||
def test_domain_detail_blocked_for_ineligible_user(self):
|
||||
"""We could easily duplicate this test for all domain management
|
||||
views, but a single url test should be solid enough since all domain
|
||||
|
@ -1974,7 +2424,7 @@ class TestDomainNameservers(TestDomainOverview):
|
|||
# the required field. form requires a minimum of 2 name servers
|
||||
self.assertContains(
|
||||
result,
|
||||
"A minimum of 2 name servers are required.",
|
||||
"At least two name servers are required.",
|
||||
count=2,
|
||||
status_code=200,
|
||||
)
|
||||
|
@ -2215,7 +2665,7 @@ class TestDomainNameservers(TestDomainOverview):
|
|||
# once around each required field.
|
||||
self.assertContains(
|
||||
result,
|
||||
"A minimum of 2 name servers are required.",
|
||||
"At least two name servers are required.",
|
||||
count=4,
|
||||
status_code=200,
|
||||
)
|
||||
|
|
|
@ -26,12 +26,23 @@ def get_domain_infos(filter_condition, sort_fields):
|
|||
|
||||
def write_row(writer, columns, domain_info):
|
||||
security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
|
||||
|
||||
# For linter
|
||||
ao = " "
|
||||
if domain_info.authorizing_official:
|
||||
first_name = domain_info.authorizing_official.first_name or ""
|
||||
last_name = domain_info.authorizing_official.last_name or ""
|
||||
ao = first_name + " " + last_name
|
||||
|
||||
security_email = " "
|
||||
if security_contacts:
|
||||
security_email = security_contacts[0].email
|
||||
|
||||
invalid_emails = {"registrar@dotgov.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
|
||||
FIELDS = {
|
||||
"Domain name": domain_info.domain.name,
|
||||
|
@ -44,13 +55,14 @@ def write_row(writer, columns, domain_info):
|
|||
"State": domain_info.state_territory,
|
||||
"AO": ao,
|
||||
"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(),
|
||||
"Expiration date": domain_info.domain.expiration_date,
|
||||
"Created at": domain_info.domain.created_at,
|
||||
"First ready": domain_info.domain.first_ready,
|
||||
"Deleted": domain_info.domain.deleted,
|
||||
}
|
||||
|
||||
writer.writerow([FIELDS.get(column, "") for column in columns])
|
||||
|
||||
|
||||
|
|
|
@ -50,8 +50,8 @@ class GenericError(Exception):
|
|||
|
||||
_error_mapping = {
|
||||
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: (
|
||||
"We’re experiencing a system connection error. Please wait a few minutes "
|
||||
"and try again. If you continue to receive this error after a few tries, "
|
||||
"We’re experiencing a system error. Please wait a few minutes "
|
||||
"and try again. If you continue to get this error, "
|
||||
"contact help@get.gov."
|
||||
),
|
||||
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
|
||||
|
@ -103,13 +103,15 @@ class NameserverError(Exception):
|
|||
"""
|
||||
|
||||
_error_mapping = {
|
||||
NameserverErrorCodes.MISSING_IP: ("Using your domain for a name server requires an IP address"),
|
||||
NameserverErrorCodes.MISSING_IP: ("Using your domain for a name server requires an IP address."),
|
||||
NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ("Name server address does not match domain name"),
|
||||
NameserverErrorCodes.INVALID_IP: ("{}: Enter an IP address in the required format."),
|
||||
NameserverErrorCodes.TOO_MANY_HOSTS: ("Too many hosts provided, you may not have more than 13 nameservers."),
|
||||
NameserverErrorCodes.MISSING_HOST: ("Name server must be provided to enter IP address."),
|
||||
NameserverErrorCodes.TOO_MANY_HOSTS: ("You can't have more than 13 nameservers."),
|
||||
NameserverErrorCodes.MISSING_HOST: ("You must provide a name server to enter an IP address."),
|
||||
NameserverErrorCodes.INVALID_HOST: ("Enter a name server in the required format, like ns1.example.com"),
|
||||
NameserverErrorCodes.DUPLICATE_HOST: ("Remove duplicate entry"),
|
||||
NameserverErrorCodes.DUPLICATE_HOST: (
|
||||
"You already entered this name server address. Name server addresses must be unique."
|
||||
),
|
||||
NameserverErrorCodes.BAD_DATA: (
|
||||
"There’s something wrong with the name server information you provided. "
|
||||
"If you need help email us at help@get.gov."
|
||||
|
@ -162,8 +164,8 @@ class DsDataError(Exception):
|
|||
),
|
||||
DsDataErrorCodes.INVALID_DIGEST_SHA1: ("SHA-1 digest must be exactly 40 characters."),
|
||||
DsDataErrorCodes.INVALID_DIGEST_SHA256: ("SHA-256 digest must be exactly 64 characters."),
|
||||
DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters [0-9,a-f]."),
|
||||
DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535"),
|
||||
DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters (0-9, a-f)."),
|
||||
DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
|
@ -193,7 +195,7 @@ class SecurityEmailError(Exception):
|
|||
"""
|
||||
|
||||
_error_mapping = {
|
||||
SecurityEmailErrorCodes.BAD_DATA: ("Enter an email address in the required format, like name@example.com.")
|
||||
SecurityEmailErrorCodes.BAD_DATA: ("Enter an email address in the required format, " "like name@example.com."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
|
|
|
@ -42,7 +42,6 @@ class Step(StrEnum):
|
|||
PURPOSE = "purpose"
|
||||
YOUR_CONTACT = "your_contact"
|
||||
OTHER_CONTACTS = "other_contacts"
|
||||
NO_OTHER_CONTACTS = "no_other_contacts"
|
||||
ANYTHING_ELSE = "anything_else"
|
||||
REQUIREMENTS = "requirements"
|
||||
REVIEW = "review"
|
||||
|
@ -74,7 +73,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
URL_NAMESPACE = "application"
|
||||
# name for accessing /application/<id>/edit
|
||||
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.
|
||||
TITLES = {
|
||||
Step.ORGANIZATION_TYPE: _("Type of organization"),
|
||||
|
@ -89,7 +88,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
Step.PURPOSE: _("Purpose of your domain"),
|
||||
Step.YOUR_CONTACT: _("Your contact information"),
|
||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||
Step.NO_OTHER_CONTACTS: _("No other employees from your organization?"),
|
||||
Step.ANYTHING_ELSE: _("Anything else?"),
|
||||
Step.REQUIREMENTS: _("Requirements for operating .gov domains"),
|
||||
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.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.NO_OTHER_CONTACTS: lambda w: w.from_model("show_no_other_contacts_rationale", False),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
@ -153,6 +150,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
def storage(self):
|
||||
# marking session as modified on every access
|
||||
# so that updates to nested keys are always saved
|
||||
# push to sandbox will remove
|
||||
self.request.session.modified = True
|
||||
return self.request.session.setdefault(self.prefix, {})
|
||||
|
||||
|
@ -488,12 +486,46 @@ class YourContact(ApplicationWizard):
|
|||
|
||||
class OtherContacts(ApplicationWizard):
|
||||
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):
|
||||
template_name = "application_no_other_contacts.html"
|
||||
forms = [forms.NoOtherContactsForm]
|
||||
# set all the required other_contact fields as necessary since new forms
|
||||
# were added through javascript
|
||||
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):
|
||||
|
|
|
@ -196,7 +196,7 @@ class DomainOrgNameAddressView(DomainFormBaseView):
|
|||
"""The form is valid, save the organization name and mailing address."""
|
||||
form.save()
|
||||
|
||||
messages.success(self.request, "The organization information has been updated.")
|
||||
messages.success(self.request, "The organization information for this domain has been updated.")
|
||||
|
||||
# superclass has the redirect
|
||||
return super().form_valid(form)
|
||||
|
@ -348,9 +348,8 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
messages.success(
|
||||
self.request,
|
||||
"The name servers for this domain have been updated. "
|
||||
"Keep in mind that DNS changes may take some time to "
|
||||
"propagate across the internet. It can take anywhere "
|
||||
"from a few minutes to 48 hours for your changes to take place.",
|
||||
"Note that DNS changes could take anywhere from a few minutes to "
|
||||
"48 hours to propagate across the internet.",
|
||||
)
|
||||
|
||||
# superclass has the redirect
|
||||
|
@ -549,7 +548,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
|
|||
# Post to DB using values from the form
|
||||
form.save()
|
||||
|
||||
messages.success(self.request, "Your contact information for this domain has been updated.")
|
||||
messages.success(self.request, "Your contact information has been updated.")
|
||||
|
||||
# superclass has the redirect
|
||||
return super().form_valid(form)
|
||||
|
@ -686,7 +685,7 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
)
|
||||
else:
|
||||
if add_success:
|
||||
messages.success(self.request, f"Invited {email} to this domain.")
|
||||
messages.success(self.request, f"{email} has been invited to this domain.")
|
||||
|
||||
def _make_invitation(self, email_address: str, requester: User):
|
||||
"""Make a Domain invitation for this email and redirect with a message."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue