mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-19 19:09:22 +02:00
merge main
This commit is contained in:
commit
60f5bbd640
107 changed files with 3021 additions and 2058 deletions
6
.github/ISSUE_TEMPLATE/bug.yml
vendored
6
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -22,7 +22,7 @@ body:
|
|||
attributes:
|
||||
label: Expected Behavior
|
||||
description: "Please add a concise description of the behavior you would expect if this issue were not occurring"
|
||||
placeholder: "Example: When submitting a new domain application, the request should be successful, OR if there is a problem with the user's application preventing submission, errors should be enumerated to the user"
|
||||
placeholder: "Example: When submitting a new domain request, the request should be successful, OR if there is a problem with the user's application preventing submission, errors should be enumerated to the user"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
@ -33,8 +33,8 @@ body:
|
|||
How can the issue be reliably reproduced? Feel free to include screenshots or other supporting artifacts
|
||||
|
||||
Example:
|
||||
1. In the test environment, fill out the application for a new domain
|
||||
2. Click the button to trigger a save/submit on the final page and complete the application
|
||||
1. In the test environment, fill out the domain request for a new domain
|
||||
2. Click the button to trigger a save/submit on the final page and complete the domain request
|
||||
3. See the error
|
||||
value: |
|
||||
1.
|
||||
|
|
8
.github/ISSUE_TEMPLATE/story.yml
vendored
8
.github/ISSUE_TEMPLATE/story.yml
vendored
|
@ -19,7 +19,7 @@ body:
|
|||
|
||||
Example:
|
||||
As an analyst
|
||||
I want the ability to approve a domain application
|
||||
I want the ability to approve a domain request
|
||||
so that a request can be fulfilled and a new .gov domain can be provisioned
|
||||
value: |
|
||||
As a
|
||||
|
@ -36,11 +36,11 @@ body:
|
|||
|
||||
Example:
|
||||
- Application sends an email when analysts approve domain requests
|
||||
- Domain application status is "approved"
|
||||
- Domain request status is "approved"
|
||||
|
||||
Example ("given, when, then" format):
|
||||
Given that I am an analyst who has finished reviewing a domain application
|
||||
When I click to approve a domain application
|
||||
Given that I am an analyst who has finished reviewing a domain request
|
||||
When I click to approve a domain request
|
||||
Then the domain provisioning process should be initiated, and the applicant should receive an email update.
|
||||
validations:
|
||||
required: true
|
||||
|
|
9
.github/workflows/daily-csv-upload.yaml
vendored
9
.github/workflows/daily-csv-upload.yaml
vendored
|
@ -31,3 +31,12 @@ jobs:
|
|||
cf_space: ${{ secrets.CF_REPORT_ENV }}
|
||||
cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_full_report' --name full"
|
||||
|
||||
- name: Generate and email domain-metadata.csv
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ secrets.CF_REPORT_ENV }}
|
||||
cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py email_current_metadata_report' --name metadata"
|
||||
|
||||
|
|
2
.github/workflows/deploy-sandbox.yaml
vendored
2
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -22,6 +22,8 @@ jobs:
|
|||
|| startsWith(github.head_ref, 'es/')
|
||||
|| startsWith(github.head_ref, 'ky/')
|
||||
|| startsWith(github.head_ref, 'backup/')
|
||||
|| startsWith(github.head_ref, 'meoward/')
|
||||
|| startsWith(github.head_ref, 'bob/')
|
||||
outputs:
|
||||
environment: ${{ steps.var.outputs.environment}}
|
||||
runs-on: "ubuntu-latest"
|
||||
|
|
2
.github/workflows/migrate.yaml
vendored
2
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,8 @@ on:
|
|||
- stable
|
||||
- staging
|
||||
- development
|
||||
- bob
|
||||
- meoward
|
||||
- backup
|
||||
- ky
|
||||
- es
|
||||
|
|
2
.github/workflows/reset-db.yaml
vendored
2
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,8 @@ on:
|
|||
options:
|
||||
- staging
|
||||
- development
|
||||
- bob
|
||||
- meoward
|
||||
- backup
|
||||
- ky
|
||||
- es
|
||||
|
|
5
.github/workflows/test-deploy.yaml
vendored
5
.github/workflows/test-deploy.yaml
vendored
|
@ -28,7 +28,6 @@ jobs:
|
|||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ENVIRONMENT: ${{ needs.variables.outputs.environment }}
|
||||
CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME
|
||||
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
||||
steps:
|
||||
|
@ -38,5 +37,5 @@ jobs:
|
|||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ env.ENVIRONMENT }}
|
||||
cf_command: "push -f ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml --strategy rolling"
|
||||
cf_space: ${{ github.event.inputs.environment }}
|
||||
cf_command: "push -f ops/manifests/manifest-${{ github.event.inputs.environment }}.yaml --strategy rolling"
|
|
@ -1,4 +1,4 @@
|
|||
# 15. Use Django-FSM library for domain application state
|
||||
# 15. Use Django-FSM library for domain request state
|
||||
|
||||
Date: 2022-11-03
|
||||
|
||||
|
@ -10,12 +10,12 @@ Accepted
|
|||
|
||||
The applications that registrants submit for domains move through a variety of
|
||||
different states or stages as they are processed by CISA staff. Traditionally,
|
||||
there would be a “domain application” data model with a “status” field. The
|
||||
there would be a “domain request” data model with a “status” field. The
|
||||
rules in the application code that control what changes are permitted to the
|
||||
statuses are called “domain logic”.
|
||||
|
||||
In a large piece of software, domain logic often spreads around the code base
|
||||
because while handling a single request like “mark this application as
|
||||
because while handling a single request like “mark this domain request as
|
||||
approved”, requirements can be enforced at many different points during the
|
||||
process.
|
||||
|
||||
|
@ -28,7 +28,7 @@ states and can change states (or “transition”) according to fixed rules.
|
|||
We will use the django-fsm library to represent the status of our domain
|
||||
registration applications as a finite state machine. The library allows us to
|
||||
list what statuses are possible and describe which state transitions are
|
||||
possible (e.g. Can an approved application ever be marked as “in-process”?).
|
||||
possible (e.g. Can an approved domain request ever be marked as “in-process”?).
|
||||
|
||||
## Consequences
|
||||
|
||||
|
|
|
@ -8,17 +8,17 @@ Accepted
|
|||
|
||||
## Context
|
||||
|
||||
The application form by which registrants apply for a .gov domain is presented over many pages.
|
||||
The domain request form by which registrants apply for a .gov domain is presented over many pages.
|
||||
|
||||
Because we use server-side rendering, each page of the application is a unique HTML page with form fields surrounded by a form tag.
|
||||
Because we use server-side rendering, each page of the domain request is a unique HTML page with form fields surrounded by a form tag.
|
||||
|
||||
Needing a way to coordinate state between the pages as a user fills in their application, we initially used the Form wizard from [django-formtools](https://django-formtools.readthedocs.io/en/latest/wizard.html). This eventually proved unworkable due to the lack of native ability to have more than one Django form object displayed on a single HTML page.
|
||||
Needing a way to coordinate state between the pages as a user fills in their domain request, we initially used the Form wizard from [django-formtools](https://django-formtools.readthedocs.io/en/latest/wizard.html). This eventually proved unworkable due to the lack of native ability to have more than one Django form object displayed on a single HTML page.
|
||||
|
||||
However, a significant portion of the user workflow had already been coded, so it seemed prudent to port some of the formtools logic into our codebase.
|
||||
|
||||
## Decision
|
||||
|
||||
To maintain each page of the domain application as its own Django view class, inheriting common code from a parent class.
|
||||
To maintain each page of the domain request as its own Django view class, inheriting common code from a parent class.
|
||||
|
||||
To maintain Django form and formset class in accordance with the Django models whose data they collect, independently of the pages on which they appear.
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ Approved
|
|||
## Context
|
||||
|
||||
Our application needs to be able to send email to applicants for various
|
||||
purposes including notifying them that their application has been submitted.
|
||||
purposes including notifying them that their domain request has been submitted.
|
||||
We need infrastructure for programmatically sending email. Amazon Web Services
|
||||
(AWS) provides the Simple Email Service (SES) that can do that. CISA can
|
||||
provide access to AWS SES for our application.
|
||||
|
|
|
@ -8,8 +8,7 @@ Accepted
|
|||
|
||||
## Context
|
||||
|
||||
CISA needs a way to perform administrative actions to manage the new get.gov application as well as the .gov domain
|
||||
application requests submitted. Analysts need to be able to view, review, and approve domain applications. Other
|
||||
CISA needs a way to perform administrative actions to manage the new get.gov application as well as the .gov domain requests submitted. Analysts need to be able to view, review, and approve domain requests. Other
|
||||
dashboard views, reports, searches (with filters and sorting) are also highly desired.
|
||||
|
||||
## Decision
|
||||
|
|
|
@ -8,13 +8,13 @@ Accepted
|
|||
|
||||
## Context
|
||||
|
||||
Historically, the .gov vendor managed initial identity verification and organizational affiliation for users that request a .gov domain. With the new registrar, _any user with a valid Login.gov account_ will be able to make a request. As a primary layer of abuse prevention (i.e., DDoSing the registry program with illegitimate requests), we need a way to stop new users from submitting multiple domain requests before they are known to the .gov registry. In this case, "known" means they have at least one approved domain application or existing domain.
|
||||
Historically, the .gov vendor managed initial identity verification and organizational affiliation for users that request a .gov domain. With the new registrar, _any user with a valid Login.gov account_ will be able to make a request. As a primary layer of abuse prevention (i.e., DDoSing the registry program with illegitimate requests), we need a way to stop new users from submitting multiple domain requests before they are known to the .gov registry. In this case, "known" means they have at least one approved domain request or existing domain.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**Option 1:** Users will not be able to submit any new applications if they have 0 prior approved applications OR prior registered .gov domains. We would add a page alert informing the user that they cannot submit their application because they have an application in one of these "3" statuses (Submitted, In Review or Action Needed). They would still be able to create and edit new applications, just not submit them. The benefits of this option are that it would allow users to have multiple applications essentially in "draft mode" that are queued up and ready for submission after they are permitted to submit.
|
||||
**Option 1:** Users will not be able to submit any new applications if they have 0 prior approved applications OR prior registered .gov domains. We would add a page alert informing the user that they cannot submit their application because they have a domain request in one of these "3" statuses (Submitted, In Review or Action Needed). They would still be able to create and edit new applications, just not submit them. The benefits of this option are that it would allow users to have multiple applications essentially in "draft mode" that are queued up and ready for submission after they are permitted to submit.
|
||||
|
||||
**Option 2:** Users will not be able to submit any new applications if they have 0 prior approved applications OR prior registered .gov domains. Additionally, we would remove the ability to edit any application with the started/withdrawn/rejected status, or start a new application. The benefit of this option is that a user would not be able to begin an action (submitting an application) that they are not allowed to complete.
|
||||
**Option 2:** Users will not be able to submit any new applications if they have 0 prior approved applications OR prior registered .gov domains. Additionally, we would remove the ability to edit any application with the started/withdrawn/rejected status, or start a new application. The benefit of this option is that a user would not be able to begin an action (submitting a domain request) that they are not allowed to complete.
|
||||
|
||||
## Decision
|
||||
|
||||
|
|
|
@ -18,13 +18,13 @@ Deployment_Node(aws, "AWS GovCloud", "Amazon Web Services Region") {
|
|||
Deployment_Node(organization, "get.gov organization") {
|
||||
Deployment_Node(sandbox, "sandbox space") {
|
||||
System_Boundary(dashboard_sandbox, "get.gov registrar") {
|
||||
Container(getgov_app_sandbox, "Registrar Application", "Python, Django", "Delivers static HTML/CSS and forms")
|
||||
Container(getgov_app_sandbox, "Registrar Domain Request", "Python, Django", "Delivers static HTML/CSS and forms")
|
||||
ContainerDb(dashboard_db_sandbox, "sandbox PostgreSQL Database", "AWS RDS", "Stores agency information and reports")
|
||||
}
|
||||
}
|
||||
Deployment_Node(stable, "stable space") {
|
||||
System_Boundary(dashboard_stable, "get.gov registrar") {
|
||||
Container(getgov_app_stable, "Registrar Application", "Python, Django", "Delivers static HTML/CSS and forms")
|
||||
Container(getgov_app_stable, "Registrar Domain Request", "Python, Django", "Delivers static HTML/CSS and forms")
|
||||
ContainerDb(dashboard_db_stable, "stable PostgreSQL Database", "AWS RDS", "Stores agency information and reports")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
This diagram connects the data models along with various workflow stages.
|
||||
|
||||
1. The applicant starts the process at `/request` interacting with the
|
||||
`DomainApplication` object.
|
||||
`DomainRequest` object.
|
||||
|
||||
2. The analyst approves the application using the `DomainApplication`'s
|
||||
2. The analyst approves the domain request using the `DomainRequest`'s
|
||||
`approve()` method which creates many related objects: `UserDomainRole`,
|
||||
`Domain`, and `DomainInformation`.
|
||||
|
||||
|
@ -36,8 +36,8 @@ $ docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg model_timeli
|
|||
allowmixing
|
||||
left to right direction
|
||||
|
||||
class DomainApplication {
|
||||
Application for a domain
|
||||
class DomainRequest {
|
||||
Request for a domain
|
||||
--
|
||||
creator (User)
|
||||
investigator (User)
|
||||
|
@ -66,7 +66,7 @@ note left of User
|
|||
<b>username</b> is the Login UUID
|
||||
end note
|
||||
|
||||
DomainApplication -l- User : creator, investigator
|
||||
DomainRequest -l- User : creator, investigator
|
||||
|
||||
class Contact {
|
||||
Contact info for a person
|
||||
|
@ -80,7 +80,7 @@ class Contact {
|
|||
--
|
||||
}
|
||||
|
||||
DomainApplication *-r-* Contact : authorizing_official, submitter, other_contacts
|
||||
DomainRequest *-r-* Contact : authorizing_official, submitter, other_contacts
|
||||
|
||||
class DraftDomain {
|
||||
Requested domain
|
||||
|
@ -89,7 +89,7 @@ class DraftDomain {
|
|||
--
|
||||
}
|
||||
|
||||
DomainApplication -l- DraftDomain : requested_domain
|
||||
DomainRequest -l- DraftDomain : requested_domain
|
||||
|
||||
class Domain {
|
||||
Approved domain
|
||||
|
@ -99,21 +99,21 @@ class Domain {
|
|||
<b>EPP methods</b>
|
||||
}
|
||||
|
||||
DomainApplication .right[#blue].> Domain : approve()
|
||||
DomainRequest .right[#blue].> Domain : approve()
|
||||
|
||||
class DomainInformation {
|
||||
Registrar information on a domain
|
||||
--
|
||||
domain (Domain)
|
||||
domain_application (DomainApplication)
|
||||
domain_request (DomainRequest)
|
||||
security_email
|
||||
--
|
||||
Request information...
|
||||
}
|
||||
|
||||
DomainInformation -- Domain
|
||||
DomainInformation -- DomainApplication
|
||||
DomainApplication .[#blue].> DomainInformation : approve()
|
||||
DomainInformation -- DomainRequest
|
||||
DomainRequest .[#blue].> DomainInformation : approve()
|
||||
|
||||
class UserDomainRole {
|
||||
Permissions
|
||||
|
@ -125,7 +125,7 @@ class UserDomainRole {
|
|||
}
|
||||
UserDomainRole -- User
|
||||
UserDomainRole -- Domain
|
||||
DomainApplication .[#blue].> UserDomainRole : approve()
|
||||
DomainRequest .[#blue].> UserDomainRole : approve()
|
||||
|
||||
class DomainInvitation {
|
||||
Email invitations sent
|
||||
|
@ -139,10 +139,10 @@ DomainInvitation -- Domain
|
|||
DomainInvitation .[#green].> UserDomainRole : User.on_each_login()
|
||||
|
||||
actor applicant #Red
|
||||
applicant -d-> DomainApplication : **/request**
|
||||
applicant -d-> DomainRequest : **/request**
|
||||
|
||||
actor analyst #Blue
|
||||
analyst -[#blue]-> DomainApplication : **approve()**
|
||||
analyst -[#blue]-> DomainRequest : **approve()**
|
||||
|
||||
actor user1 #Green
|
||||
user1 -[#green]-> Domain : **/domain/<id>/nameservers**
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
@ -39,8 +39,8 @@ class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
|
|||
registrar.Contact -- registrar.User
|
||||
|
||||
|
||||
class "registrar.DomainApplication <Registrar>" as registrar.DomainApplication #d6f4e9 {
|
||||
domain application
|
||||
class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
||||
domain request
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
|
@ -77,15 +77,15 @@ class "registrar.DomainApplication <Registrar>" as registrar.DomainApplication #
|
|||
# other_contacts (ManyToManyField)
|
||||
--
|
||||
}
|
||||
registrar.DomainApplication -- registrar.User
|
||||
registrar.DomainApplication -- registrar.User
|
||||
registrar.DomainApplication -- registrar.Contact
|
||||
registrar.DomainApplication -- registrar.DraftDomain
|
||||
registrar.DomainApplication -- registrar.Domain
|
||||
registrar.DomainApplication -- registrar.Contact
|
||||
registrar.DomainApplication *--* registrar.Website
|
||||
registrar.DomainApplication *--* registrar.Website
|
||||
registrar.DomainApplication *--* registrar.Contact
|
||||
registrar.DomainRequest -- registrar.User
|
||||
registrar.DomainRequest -- registrar.User
|
||||
registrar.DomainRequest -- registrar.Contact
|
||||
registrar.DomainRequest -- registrar.DraftDomain
|
||||
registrar.DomainRequest -- registrar.Domain
|
||||
registrar.DomainRequest -- registrar.Contact
|
||||
registrar.DomainRequest *--* registrar.Website
|
||||
registrar.DomainRequest *--* registrar.Website
|
||||
registrar.DomainRequest *--* registrar.Contact
|
||||
|
||||
|
||||
class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #d6f4e9 {
|
||||
|
@ -95,7 +95,7 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
|
|||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
~ creator (ForeignKey)
|
||||
~ domain_application (OneToOneField)
|
||||
~ domain_request (OneToOneField)
|
||||
+ organization_type (CharField)
|
||||
+ federally_recognized_tribe (BooleanField)
|
||||
+ state_recognized_tribe (BooleanField)
|
||||
|
@ -124,7 +124,7 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
|
|||
--
|
||||
}
|
||||
registrar.DomainInformation -- registrar.User
|
||||
registrar.DomainInformation -- registrar.DomainApplication
|
||||
registrar.DomainInformation -- registrar.DomainRequest
|
||||
registrar.DomainInformation -- registrar.Contact
|
||||
registrar.DomainInformation -- registrar.Domain
|
||||
registrar.DomainInformation -- registrar.Contact
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
@ -78,7 +78,7 @@ Get the secrets from Cloud.gov by running `cf env getgov-YOURSANDBOX`. More info
|
|||
The endpoint /admin can be used to view and manage site content, including but not limited to user information and the list of current applications in the database. To be able to view and use /admin locally:
|
||||
|
||||
1. Login via login.gov
|
||||
2. Go to the home page and make sure you can see the part where you can submit an application
|
||||
2. Go to the home page and make sure you can see the part where you can submit a domain request
|
||||
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4
|
||||
4. in src/registrar/fixtures_users.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below:
|
||||
|
||||
|
@ -93,14 +93,14 @@ The endpoint /admin can be used to view and manage site content, including but n
|
|||
]
|
||||
```
|
||||
|
||||
5. In the browser, navigate to /admin. To verify that all is working correctly, under "domain applications" you should see fake domains with various fake statuses.
|
||||
5. In the browser, navigate to /admin. To verify that all is working correctly, under "domain requests" you should see fake domains with various fake statuses.
|
||||
6. Add an optional email key/value pair
|
||||
|
||||
### Adding an Analyst to /admin
|
||||
Analysts are a variant of the admin role with limited permissions. The process for adding an Analyst is much the same as adding an admin:
|
||||
|
||||
1. Login via login.gov (if you already exist as an admin, you will need to create a separate login.gov account for this: i.e. first.last+1@email.com)
|
||||
2. Go to the home page and make sure you can see the part where you can submit an application
|
||||
2. Go to the home page and make sure you can see the part where you can submit a domain request
|
||||
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 (this will be a different UUID than the one obtained from creating an admin)
|
||||
4. in src/registrar/fixtures_users.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below:
|
||||
|
||||
|
@ -145,7 +145,7 @@ You can change the logging verbosity, if needed. Do a web search for "django log
|
|||
|
||||
## Mock data
|
||||
|
||||
[load.py](../../src/registrar/management/commands/load.py) called from docker-compose (locally) and reset-db.yml (upper) loads the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_applications.py](../../src/registrar/fixtures_applications.py), giving you some test data to play with while developing.
|
||||
[load.py](../../src/registrar/management/commands/load.py) called from docker-compose (locally) and reset-db.yml (upper) loads the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_domain_requests.py](../../src/registrar/fixtures_domain_requests.py), giving you some test data to play with while developing.
|
||||
|
||||
See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures.
|
||||
|
||||
|
@ -330,11 +330,12 @@ To associate a S3 instance to your sandbox, follow these steps:
|
|||
3. Click `Services` on the application nav bar
|
||||
4. Add a new service (plus symbol)
|
||||
5. Click `Marketplace Service`
|
||||
6. On the `Select the service` dropdown, select `s3`
|
||||
7. Under the dropdown on `Select Plan`, select `basic-sandbox`
|
||||
8. Under `Service Instance` enter `getgov-s3` for the name
|
||||
6. For Space, put in your sandbox initials
|
||||
7. On the `Select the service` dropdown, select `s3`
|
||||
8. Under the dropdown on `Select Plan`, select `basic-sandbox`
|
||||
9. Under `Service Instance` enter `getgov-s3` for the name and leave the other fields empty
|
||||
|
||||
See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI.
|
||||
See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI.
|
||||
|
||||
### Testing your S3 instance locally
|
||||
To test the S3 bucket associated with your sandbox, you will need to add four additional variables to your `.env` file. These are as follows:
|
||||
|
|
|
@ -24,8 +24,8 @@
|
|||
## Status Change Approved
|
||||
- Starting Location: Django Admin
|
||||
- Workflow: Analyst Admin
|
||||
- Workflow Step: Click "Domain applications" -> Click an application in a status of "submitted", "In review", "rejected", or "ineligible" -> Click status dropdown -> (select "approved") -> 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 "approved". This will send you an email.
|
||||
- Workflow Step: Click "domain requests" -> Click a domain request in a status of "submitted", "In review", "rejected", or "ineligible" -> Click status dropdown -> (select "approved") -> 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 a domain request, then set the status to "approved". 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 Approved Subject
|
||||
|
@ -35,8 +35,8 @@
|
|||
## Status Change Rejected
|
||||
- Starting Location: Django Admin
|
||||
- Workflow: Analyst Admin
|
||||
- Workflow Step: Click "Domain applications" -> Click an application in a status of "In review", or "approved" -> Click status dropdown -> (select "rejected") -> 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" (and click save). Then, go back to the same application and set the status to "rejected". This will send you an email.
|
||||
- Workflow Step: Click "domain requests" -> Click a domain request in a status of "In review", or "approved" -> Click status dropdown -> (select "rejected") -> 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 a domain request, then set the status to "in review" (and click save). Then, go back to the same application and set the status to "rejected". This will send you an email.
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_rejected.txt)
|
||||
|
||||
### Status Change Rejected Subject
|
||||
|
|
|
@ -114,7 +114,7 @@ that can be used for specific tasks.
|
|||
## Cloud.gov dashboard
|
||||
|
||||
At <https://dashboard.fr.cloud.gov/applications> there is a list for all of the
|
||||
applications that a Cloud.gov user has access to. Clicking on an application
|
||||
applications that a Cloud.gov user has access to. Clicking on a domain request
|
||||
goes to a screen for that individual application, e.g.
|
||||
<https://dashboard.fr.cloud.gov/applications/2oBn9LBurIXUNpfmtZCQTCHnxUM/53b88024-1492-46aa-8fb6-1429bdb35f95/summary>.
|
||||
On that page is a left-hand link for "Log Stream" e.g.
|
||||
|
|
|
@ -117,3 +117,11 @@ You'll need to give the new certificate to the registry vendor _before_ rotating
|
|||
## REGISTRY_HOSTNAME
|
||||
|
||||
This is the hostname at which the registry can be found.
|
||||
|
||||
## SECRET_METADATA_KEY
|
||||
|
||||
This is the passphrase for the zipped and encrypted metadata email that is sent out daily. Reach out to product team members or leads with access to security passwords if the passcode is needed.
|
||||
|
||||
To change the password, use a password generator to generate a password, then update the user credentials per the above instructions. Be sure to update the [KBDX](https://docs.google.com/document/d/1_BbJmjYZNYLNh4jJPPnUEG9tFCzJrOc0nMrZrnSKKyw) file in Google Drive with this password change.
|
||||
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
========================
|
||||
|
||||
1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers
|
||||
|
||||
2. Run
|
||||
2. Run `docker-compose stop` to spin down the current containers and images so we can start afresh
|
||||
3. Run
|
||||
|
||||
cd src
|
||||
docker-compose run app bash -c "pipenv lock && pipenv requirements > requirements.txt"
|
||||
|
@ -13,9 +13,9 @@
|
|||
It is necessary to use `bash -c` because `run pipenv requirements` will not recognize that it is running non-interactively and will include garbage formatting characters.
|
||||
|
||||
The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository.
|
||||
3. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt.
|
||||
4. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt.
|
||||
This is done by either saving what it was originally or opening a PR and using that as a reference to undo changes to any mention of geventconnpool.
|
||||
Geventconnpool, when set as a requirement without the reference portion, is defaulting to get a commit from 2014 which then breaks the code, as we want the newest version from them.
|
||||
4. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies.
|
||||
5. Run `docker-compose build` to build a new image for local development with the updated dependencies.
|
||||
|
||||
The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less.
|
32
ops/manifests/manifest-bob.yaml
Normal file
32
ops/manifests/manifest-bob.yaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-bob
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-bob.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
- route: getgov-bob.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-bob-database
|
32
ops/manifests/manifest-meoward.yaml
Normal file
32
ops/manifests/manifest-meoward.yaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-meoward
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-meoward.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
- route: getgov-meoward.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-meoward-database
|
|
@ -29,6 +29,7 @@ django-login-required-middleware = "*"
|
|||
greenlet = "*"
|
||||
gevent = "*"
|
||||
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
|
||||
pyzipper="*"
|
||||
tblib = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
@ -44,4 +45,4 @@ django-webtest = "*"
|
|||
types-cachetools = "*"
|
||||
boto3-mocking = "*"
|
||||
boto3-stubs = "*"
|
||||
django-model2puml = "*"
|
||||
django-model2puml = "*"
|
65
src/Pipfile.lock
generated
65
src/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b5d93b1b9ccafc37019276a222957544bab3f1f46b5dab8a0f2ffc2e5c9e1678"
|
||||
"sha256": "082a951f15bb26a28f2dca7e0840fdf61518b3d90c42d77a310f982344cbd1dc"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
|
@ -32,20 +32,20 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
|
||||
"sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
|
||||
"sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714",
|
||||
"sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
|
||||
"sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
|
||||
"sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f",
|
||||
"sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
|
@ -376,20 +376,20 @@
|
|||
"django"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:cc421ddb143fa30183568164755aa113a160e555cd19e97e664c478662032c24",
|
||||
"sha256:feeaf28f17fd0499f9cd7c0fcf408c6d82c308e69e335eb92d09322fc9ed8138"
|
||||
"sha256:069727a8f73d8ba8d033d3cd95c0da231d44f38f1da773bf076cef168d312ee8",
|
||||
"sha256:e0bcfd41c718c07a7db422f9109e490746450da38793fe4ee197f397b9343435"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==10.3.0"
|
||||
"version": "==11.0.0"
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267",
|
||||
"sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de"
|
||||
"sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1",
|
||||
"sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==23.3.0"
|
||||
"version": "==24.0.0"
|
||||
},
|
||||
"fred-epplib": {
|
||||
"git": "https://github.com/cisagov/epplib.git",
|
||||
|
@ -708,11 +708,11 @@
|
|||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b",
|
||||
"sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd"
|
||||
"sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3",
|
||||
"sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.21.0"
|
||||
"version": "==3.21.1"
|
||||
},
|
||||
"oic": {
|
||||
"hashes": [
|
||||
|
@ -994,6 +994,15 @@
|
|||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"pyzipper": {
|
||||
"hashes": [
|
||||
"sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc",
|
||||
"sha256:6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==0.3.6"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||
|
@ -1186,12 +1195,12 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
|
||||
"sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
|
||||
"sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714",
|
||||
"sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"boto3-mocking": {
|
||||
"hashes": [
|
||||
|
@ -1204,28 +1213,28 @@
|
|||
},
|
||||
"boto3-stubs": {
|
||||
"hashes": [
|
||||
"sha256:7db5194e47f76e0010cd00b6ad9725db114d6a3fd04e52ceed3ef1181fe326bc",
|
||||
"sha256:c7b2e8b99f4896cf1226df47d4badaaa8df7426008c96a428bf00205695669e9"
|
||||
"sha256:627f8eca69d832581ee1676d39df099a2a2e3a86d6b3ebd21c81c5f11ed6a6fa",
|
||||
"sha256:a87e7ecbab6235ec371b4363027e57483bca349a9cd5c891f40db81dadfa273e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
|
||||
"sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
|
||||
"sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f",
|
||||
"sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"botocore-stubs": {
|
||||
"hashes": [
|
||||
"sha256:958f0084322dc9e549f73151b686fa51b15858fb2b3a573b9f4367f073fff463",
|
||||
"sha256:bcc35bfbd14d1261813681c40108f2ce85fdf082c15b0a04016d3c22dd93b73f"
|
||||
"sha256:018e001e3add5eb1828ef444b45fb8c9faf695e08334031bf2d96853cd9af703",
|
||||
"sha256:25468ba6983987b704b1856bb155f297f576e6d4a690b021ab0c7122889ba907"
|
||||
],
|
||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
|
|
|
@ -58,6 +58,8 @@ services:
|
|||
- AWS_S3_SECRET_ACCESS_KEY
|
||||
- AWS_S3_REGION
|
||||
- AWS_S3_BUCKET_NAME
|
||||
# File encryption credentials
|
||||
- SECRET_ENCRYPT_METADATA
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from datetime import date
|
||||
import logging
|
||||
import datetime
|
||||
import copy
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Avg, F, Value, CharField, Q
|
||||
|
@ -15,8 +16,9 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.urls import reverse
|
||||
from dateutil.relativedelta import relativedelta # type: ignore
|
||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||
from registrar.models import Contact, Domain, DomainApplication, DraftDomain, User, Website
|
||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
||||
from registrar.utility import csv_export
|
||||
from registrar.utility.errors import FSMApplicationError, FSMErrorCodes
|
||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||
from django.contrib.admin.views.main import ORDER_VAR
|
||||
from registrar.widgets import NoAutocompleteFilteredSelectMultiple
|
||||
|
@ -70,12 +72,12 @@ class DomainInformationInlineForm(forms.ModelForm):
|
|||
}
|
||||
|
||||
|
||||
class DomainApplicationAdminForm(forms.ModelForm):
|
||||
class DomainRequestAdminForm(forms.ModelForm):
|
||||
"""Custom form to limit transitions to available transitions.
|
||||
This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||
|
||||
class Meta:
|
||||
model = models.DomainApplication
|
||||
model = models.DomainRequest
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"current_websites": NoAutocompleteFilteredSelectMultiple("current_websites", False),
|
||||
|
@ -86,26 +88,98 @@ class DomainApplicationAdminForm(forms.ModelForm):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
application = kwargs.get("instance")
|
||||
if application and application.pk:
|
||||
current_state = application.status
|
||||
domain_request = kwargs.get("instance")
|
||||
if domain_request and domain_request.pk:
|
||||
current_state = domain_request.status
|
||||
|
||||
# first option in status transitions is current state
|
||||
available_transitions = [(current_state, application.get_status_display())]
|
||||
available_transitions = [(current_state, domain_request.get_status_display())]
|
||||
|
||||
transitions = get_available_FIELD_transitions(
|
||||
application, models.DomainApplication._meta.get_field("status")
|
||||
)
|
||||
if domain_request.investigator is not None:
|
||||
transitions = get_available_FIELD_transitions(
|
||||
domain_request, models.DomainRequest._meta.get_field("status")
|
||||
)
|
||||
else:
|
||||
transitions = self.get_custom_field_transitions(
|
||||
domain_request, models.DomainRequest._meta.get_field("status")
|
||||
)
|
||||
|
||||
for transition in transitions:
|
||||
available_transitions.append((transition.target, transition.target.label))
|
||||
|
||||
# only set the available transitions if the user is not restricted
|
||||
# from editing the domain application; otherwise, the form will be
|
||||
# from editing the domain request; otherwise, the form will be
|
||||
# readonly and the status field will not have a widget
|
||||
if not application.creator.is_restricted():
|
||||
if not domain_request.creator.is_restricted():
|
||||
self.fields["status"].widget.choices = available_transitions
|
||||
|
||||
def get_custom_field_transitions(self, instance, field):
|
||||
"""Custom implementation of get_available_FIELD_transitions
|
||||
in the FSM. Allows us to still display fields filtered out by a condition."""
|
||||
curr_state = field.get_state(instance)
|
||||
transitions = field.transitions[instance.__class__]
|
||||
|
||||
for name, transition in transitions.items():
|
||||
meta = transition._django_fsm
|
||||
if meta.has_transition(curr_state):
|
||||
yield meta.get_transition(curr_state)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Override of the default clean on the form.
|
||||
This is so we can inject custom form-level error messages.
|
||||
"""
|
||||
# clean is called from clean_forms, which is called from is_valid
|
||||
# after clean_fields. it is used to determine form level errors.
|
||||
# is_valid is typically called from view during a post
|
||||
cleaned_data = super().clean()
|
||||
status = cleaned_data.get("status")
|
||||
investigator = cleaned_data.get("investigator")
|
||||
|
||||
# Get the old status
|
||||
initial_status = self.initial.get("status", None)
|
||||
|
||||
# We only care about investigator when in these statuses
|
||||
checked_statuses = [
|
||||
DomainRequest.DomainRequestStatus.APPROVED,
|
||||
DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||
]
|
||||
|
||||
# If a status change occured, check for validity
|
||||
if status != initial_status and status in checked_statuses:
|
||||
# Checks the "investigators" field for validity.
|
||||
# That field must obey certain conditions when an domain request is approved.
|
||||
# Will call "add_error" if any issues are found.
|
||||
self._check_for_valid_investigator(investigator)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def _check_for_valid_investigator(self, investigator) -> bool:
|
||||
"""
|
||||
Checks if the investigator field is not none, and is staff.
|
||||
Adds form errors on failure.
|
||||
"""
|
||||
|
||||
is_valid = False
|
||||
|
||||
# Check if an investigator is assigned. No approval is possible without one.
|
||||
error_message = None
|
||||
if investigator is None:
|
||||
# Lets grab the error message from a common location
|
||||
error_message = FSMApplicationError.get_error_message(FSMErrorCodes.NO_INVESTIGATOR)
|
||||
elif not investigator.is_staff:
|
||||
error_message = FSMApplicationError.get_error_message(FSMErrorCodes.INVESTIGATOR_NOT_STAFF)
|
||||
else:
|
||||
is_valid = True
|
||||
|
||||
if error_message is not None:
|
||||
self.add_error("investigator", error_message)
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
# Based off of this excellent example: https://djangosnippets.org/snippets/10471/
|
||||
class MultiFieldSortableChangeList(admin.views.main.ChangeList):
|
||||
|
@ -219,8 +293,8 @@ class AdminSortFields:
|
|||
"alternative_domains": (Website, "website"),
|
||||
# == DraftDomain == #
|
||||
"requested_domain": (DraftDomain, "name"),
|
||||
# == DomainApplication == #
|
||||
"domain_application": (DomainApplication, "requested_domain__name"),
|
||||
# == DomainRequest == #
|
||||
"domain_request": (DomainRequest, "requested_domain__name"),
|
||||
# == Domain == #
|
||||
"domain": (Domain, "name"),
|
||||
"approved_domain": (Domain, "name"),
|
||||
|
@ -584,7 +658,7 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
def get_search_results(self, request, queryset, search_term):
|
||||
"""
|
||||
Override for get_search_results. This affects any upstream model using autocomplete_fields,
|
||||
such as DomainApplication. This is because autocomplete_fields uses an API call to fetch data,
|
||||
such as DomainRequest. This is because autocomplete_fields uses an API call to fetch data,
|
||||
and this fetch comes from this method.
|
||||
"""
|
||||
# Custom filtering logic
|
||||
|
@ -598,13 +672,13 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
request_get = request.GET
|
||||
|
||||
# The request defines model name and field name.
|
||||
# For instance, model_name could be "DomainApplication"
|
||||
# For instance, model_name could be "DomainRequest"
|
||||
# and field_name could be "investigator".
|
||||
model_name = request_get.get("model_name", None)
|
||||
field_name = request_get.get("field_name", None)
|
||||
|
||||
# Make sure we're only modifying requests from these models.
|
||||
models_to_target = {"domainapplication"}
|
||||
models_to_target = {"domainrequest"}
|
||||
if model_name in models_to_target:
|
||||
# Define rules per field
|
||||
match field_name:
|
||||
|
@ -895,18 +969,21 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by domain."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["creator", "domain_application", "notes"]}),
|
||||
(None, {"fields": ["creator", "submitter", "domain_request", "notes"]}),
|
||||
(".gov domain", {"fields": ["domain"]}),
|
||||
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||
("Background info", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
"fields": [
|
||||
"organization_type",
|
||||
"is_election_board",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"tribe_name",
|
||||
"federal_agency",
|
||||
"federal_type",
|
||||
"is_election_board",
|
||||
"about_your_organization",
|
||||
]
|
||||
},
|
||||
|
@ -916,28 +993,15 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
{
|
||||
"fields": [
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state_territory",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
},
|
||||
),
|
||||
("Authorizing official", {"fields": ["authorizing_official"]}),
|
||||
(".gov domain", {"fields": ["domain"]}),
|
||||
("Your contact information", {"fields": ["submitter"]}),
|
||||
("Other employees from your organization?", {"fields": ["other_contacts"]}),
|
||||
(
|
||||
"No other employees from your organization?",
|
||||
{"fields": ["no_other_contacts_rationale"]},
|
||||
),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating a .gov domain",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
),
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
|
@ -946,7 +1010,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"type_of_work",
|
||||
"more_organization_information",
|
||||
"domain",
|
||||
"domain_application",
|
||||
"domain_request",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
|
@ -959,7 +1023,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
|
||||
autocomplete_fields = [
|
||||
"creator",
|
||||
"domain_application",
|
||||
"domain_request",
|
||||
"authorizing_official",
|
||||
"domain",
|
||||
"submitter",
|
||||
|
@ -984,10 +1048,10 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
return readonly_fields # Read-only fields for analysts
|
||||
|
||||
|
||||
class DomainApplicationAdmin(ListHeaderAdmin):
|
||||
"""Custom domain applications admin class."""
|
||||
class DomainRequestAdmin(ListHeaderAdmin):
|
||||
"""Custom domain requests admin class."""
|
||||
|
||||
form = DomainApplicationAdminForm
|
||||
form = DomainRequestAdminForm
|
||||
|
||||
class InvestigatorFilter(admin.SimpleListFilter):
|
||||
"""Custom investigator filter that only displays users with the manager role"""
|
||||
|
@ -1002,7 +1066,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
"""
|
||||
# Select all investigators that are staff, then order by name and email
|
||||
privileged_users = (
|
||||
DomainApplication.objects.select_related("investigator")
|
||||
DomainRequest.objects.select_related("investigator")
|
||||
.filter(investigator__is_staff=True)
|
||||
.order_by("investigator__first_name", "investigator__last_name", "investigator__email")
|
||||
)
|
||||
|
@ -1060,7 +1124,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
"custom_election_board",
|
||||
"city",
|
||||
"state_territory",
|
||||
"created_at",
|
||||
"submission_date",
|
||||
"submitter",
|
||||
"investigator",
|
||||
]
|
||||
|
@ -1097,18 +1161,34 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by domain or submitter."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": [
|
||||
"status",
|
||||
"rejection_reason",
|
||||
"investigator",
|
||||
"creator",
|
||||
"submitter",
|
||||
"approved_domain",
|
||||
"notes",
|
||||
]
|
||||
},
|
||||
),
|
||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
||||
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
"fields": [
|
||||
"organization_type",
|
||||
"is_election_board",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"tribe_name",
|
||||
"federal_agency",
|
||||
"federal_type",
|
||||
"is_election_board",
|
||||
"about_your_organization",
|
||||
]
|
||||
},
|
||||
|
@ -1118,30 +1198,15 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
{
|
||||
"fields": [
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state_territory",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
},
|
||||
),
|
||||
("Authorizing official", {"fields": ["authorizing_official"]}),
|
||||
("Current websites", {"fields": ["current_websites"]}),
|
||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
||||
("Purpose of your domain", {"fields": ["purpose"]}),
|
||||
("Your contact information", {"fields": ["submitter"]}),
|
||||
("Other employees from your organization?", {"fields": ["other_contacts"]}),
|
||||
(
|
||||
"No other employees from your organization?",
|
||||
{"fields": ["no_other_contacts_rationale"]},
|
||||
),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating a .gov domain",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
),
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
|
@ -1172,86 +1237,147 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
|
||||
# Trigger action when a fieldset is changed
|
||||
def save_model(self, request, obj, form, change):
|
||||
if obj and obj.creator.status != models.User.RESTRICTED:
|
||||
if change: # Check if the application is being edited
|
||||
# Get the original application from the database
|
||||
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
||||
"""Custom save_model definition that handles edge cases"""
|
||||
|
||||
if (
|
||||
obj
|
||||
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
|
||||
and obj.status != models.DomainApplication.ApplicationStatus.APPROVED
|
||||
and not obj.domain_is_not_active()
|
||||
):
|
||||
# If an admin tried to set an approved application to
|
||||
# another status and the related domain is already
|
||||
# active, shortcut the action and throw a friendly
|
||||
# error message. This action would still not go through
|
||||
# shortcut or not as the rules are duplicated on the model,
|
||||
# but the error would be an ugly Django error screen.
|
||||
# == Check that the obj is in a valid state == #
|
||||
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
# If obj is none, something went very wrong.
|
||||
# The form should have blocked this, so lets forbid it.
|
||||
if not obj:
|
||||
logger.error(f"Invalid value for obj ({obj})")
|
||||
messages.set_level(request, messages.ERROR)
|
||||
messages.error(
|
||||
request,
|
||||
"Could not save DomainRequest. Something went wrong.",
|
||||
)
|
||||
return None
|
||||
|
||||
messages.error(
|
||||
request,
|
||||
"This action is not permitted. The domain is already active.",
|
||||
)
|
||||
|
||||
elif (
|
||||
obj
|
||||
and obj.status == models.DomainApplication.ApplicationStatus.REJECTED
|
||||
and not obj.rejection_reason
|
||||
):
|
||||
# This condition should never be triggered.
|
||||
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
||||
# because we clean up the rejection reason in the transition in the model.
|
||||
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
|
||||
messages.error(
|
||||
request,
|
||||
"A rejection reason is required.",
|
||||
)
|
||||
|
||||
else:
|
||||
if obj.status != original_obj.status:
|
||||
status_method_mapping = {
|
||||
models.DomainApplication.ApplicationStatus.STARTED: None,
|
||||
models.DomainApplication.ApplicationStatus.SUBMITTED: obj.submit,
|
||||
models.DomainApplication.ApplicationStatus.IN_REVIEW: obj.in_review,
|
||||
models.DomainApplication.ApplicationStatus.ACTION_NEEDED: obj.action_needed,
|
||||
models.DomainApplication.ApplicationStatus.APPROVED: obj.approve,
|
||||
models.DomainApplication.ApplicationStatus.WITHDRAWN: obj.withdraw,
|
||||
models.DomainApplication.ApplicationStatus.REJECTED: obj.reject,
|
||||
models.DomainApplication.ApplicationStatus.INELIGIBLE: (obj.reject_with_prejudice),
|
||||
}
|
||||
selected_method = status_method_mapping.get(obj.status)
|
||||
if selected_method is None:
|
||||
logger.warning("Unknown status selected in django admin")
|
||||
else:
|
||||
# This is an fsm in model which will throw an error if the
|
||||
# transition condition is violated, so we roll back the
|
||||
# status to what it was before the admin user changed it and
|
||||
# let the fsm method set it.
|
||||
obj.status = original_obj.status
|
||||
selected_method()
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
else:
|
||||
# If the user is restricted or we're saving an invalid model,
|
||||
# forbid this action.
|
||||
if not obj or obj.creator.status == models.User.RESTRICTED:
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
|
||||
messages.error(
|
||||
request,
|
||||
"This action is not permitted for applications with a restricted creator.",
|
||||
"This action is not permitted for domain requests with a restricted creator.",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# == Check if we're making a change or not == #
|
||||
|
||||
# If we're not making a change (adding a record), run save model as we do normally
|
||||
if not change:
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
# == Handle non-status changes == #
|
||||
|
||||
# Get the original domain request from the database.
|
||||
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
||||
if obj.status == original_obj.status:
|
||||
# If the status hasn't changed, let the base function take care of it
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
# == Handle status changes == #
|
||||
|
||||
# Run some checks on the current object for invalid status changes
|
||||
obj, should_save = self._handle_status_change(request, obj, original_obj)
|
||||
|
||||
# We should only save if we don't display any errors in the step above.
|
||||
if should_save:
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
def _handle_status_change(self, request, obj, original_obj):
|
||||
"""
|
||||
Checks for various conditions when a status change is triggered.
|
||||
In the event that it is valid, the status will be mapped to
|
||||
the appropriate method.
|
||||
|
||||
In the event that we should not status change, an error message
|
||||
will be displayed.
|
||||
|
||||
Returns a tuple: (obj: DomainRequest, should_proceed: bool)
|
||||
"""
|
||||
|
||||
should_proceed = True
|
||||
error_message = None
|
||||
|
||||
# Get the method that should be run given the status
|
||||
selected_method = self.get_status_method_mapping(obj)
|
||||
if selected_method is None:
|
||||
logger.warning("Unknown status selected in django admin")
|
||||
|
||||
# If the status is not mapped properly, saving could cause
|
||||
# weird issues down the line. Instead, we should block this.
|
||||
should_proceed = False
|
||||
return should_proceed
|
||||
|
||||
request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
||||
if request_is_not_approved and not obj.domain_is_not_active():
|
||||
# If an admin tried to set an approved domain request to
|
||||
# another status and the related domain is already
|
||||
# active, shortcut the action and throw a friendly
|
||||
# error message. This action would still not go through
|
||||
# shortcut or not as the rules are duplicated on the model,
|
||||
# but the error would be an ugly Django error screen.
|
||||
error_message = "This action is not permitted. The domain is already active."
|
||||
elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason:
|
||||
# This condition should never be triggered.
|
||||
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
||||
# because we clean up the rejection reason in the transition in the model.
|
||||
error_message = "A rejection reason is required."
|
||||
else:
|
||||
# This is an fsm in model which will throw an error if the
|
||||
# transition condition is violated, so we roll back the
|
||||
# status to what it was before the admin user changed it and
|
||||
# let the fsm method set it.
|
||||
obj.status = original_obj.status
|
||||
|
||||
# Try to perform the status change.
|
||||
# Catch FSMApplicationError's and return the message,
|
||||
# as these are typically user errors.
|
||||
try:
|
||||
selected_method()
|
||||
except FSMApplicationError as err:
|
||||
logger.warning(f"An error encountered when trying to change status: {err}")
|
||||
error_message = err.message
|
||||
|
||||
if error_message is not None:
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
# Display the error
|
||||
messages.error(
|
||||
request,
|
||||
error_message,
|
||||
)
|
||||
|
||||
# If an error message exists, we shouldn't proceed
|
||||
should_proceed = False
|
||||
|
||||
return (obj, should_proceed)
|
||||
|
||||
def get_status_method_mapping(self, domain_request):
|
||||
"""Returns what method should be ran given an domain request object"""
|
||||
# Define a per-object mapping
|
||||
status_method_mapping = {
|
||||
models.DomainRequest.DomainRequestStatus.STARTED: None,
|
||||
models.DomainRequest.DomainRequestStatus.SUBMITTED: domain_request.submit,
|
||||
models.DomainRequest.DomainRequestStatus.IN_REVIEW: domain_request.in_review,
|
||||
models.DomainRequest.DomainRequestStatus.ACTION_NEEDED: domain_request.action_needed,
|
||||
models.DomainRequest.DomainRequestStatus.APPROVED: domain_request.approve,
|
||||
models.DomainRequest.DomainRequestStatus.WITHDRAWN: domain_request.withdraw,
|
||||
models.DomainRequest.DomainRequestStatus.REJECTED: domain_request.reject,
|
||||
models.DomainRequest.DomainRequestStatus.INELIGIBLE: (domain_request.reject_with_prejudice),
|
||||
}
|
||||
|
||||
# Grab the method
|
||||
return status_method_mapping.get(domain_request.status, None)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 2 conditions that determine which fields are read-only:
|
||||
admin user permissions and the application creator's status, so
|
||||
admin user permissions and the domain request creator's status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
@ -1276,7 +1402,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
if obj and obj.creator.status == models.User.RESTRICTED:
|
||||
messages.warning(
|
||||
request,
|
||||
"Cannot edit an application with a restricted creator.",
|
||||
"Cannot edit a domain request with a restricted creator.",
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
|
@ -1312,7 +1438,13 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
model = models.DomainInformation
|
||||
|
||||
fieldsets = DomainInformationAdmin.fieldsets
|
||||
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
|
||||
# remove .gov domain from fieldset
|
||||
for index, (title, f) in enumerate(fieldsets):
|
||||
if title == ".gov domain":
|
||||
del fieldsets[index]
|
||||
break
|
||||
|
||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
||||
# to activate the edit/delete/view buttons
|
||||
|
@ -1320,7 +1452,7 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
autocomplete_fields = [
|
||||
"creator",
|
||||
"domain_application",
|
||||
"domain_request",
|
||||
"authorizing_official",
|
||||
"domain",
|
||||
"submitter",
|
||||
|
@ -1781,6 +1913,6 @@ admin.site.register(models.DraftDomain, DraftDomainAdmin)
|
|||
admin.site.register(models.Host, MyHostAdmin)
|
||||
admin.site.register(models.Website, WebsiteAdmin)
|
||||
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||
admin.site.register(models.DomainRequest, DomainRequestAdmin)
|
||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||
|
|
|
@ -136,12 +136,18 @@ html[data-theme="dark"] {
|
|||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#branding h1,
|
||||
h1, h2, h3,
|
||||
.module h2 {
|
||||
font-weight: font-weight('bold');
|
||||
}
|
||||
|
||||
div#content > h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.module h3 {
|
||||
padding: 0;
|
||||
color: var(--link-fg);
|
||||
|
@ -276,11 +282,6 @@ h1, h2, h3,
|
|||
}
|
||||
}
|
||||
|
||||
// Hides the "clear" button on autocomplete, as we already have one to use
|
||||
.select2-selection__clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Fixes a display issue where the list was entirely white, or had too much whitespace
|
||||
.select2-dropdown {
|
||||
display: inline-grid !important;
|
||||
|
|
|
@ -74,6 +74,9 @@ secret_aws_s3_key_id = secret("access_key_id", None) or secret("AWS_S3_ACCESS_KE
|
|||
secret_aws_s3_key = secret("secret_access_key", None) or secret("AWS_S3_SECRET_ACCESS_KEY", None)
|
||||
secret_aws_s3_bucket_name = secret("bucket", None) or secret("AWS_S3_BUCKET_NAME", None)
|
||||
|
||||
# Passphrase for the encrypted metadata email
|
||||
secret_encrypt_metadata = secret("SECRET_ENCRYPT_METADATA", None)
|
||||
|
||||
secret_registry_cl_id = secret("REGISTRY_CL_ID")
|
||||
secret_registry_password = secret("REGISTRY_PASSWORD")
|
||||
secret_registry_cert = b64decode(secret("REGISTRY_CERT", ""))
|
||||
|
@ -94,6 +97,7 @@ DEBUG = env_debug
|
|||
|
||||
# Controls production specific feature toggles
|
||||
IS_PRODUCTION = env_is_production
|
||||
SECRET_ENCRYPT_METADATA = secret_encrypt_metadata
|
||||
|
||||
# Applications are modular pieces of code.
|
||||
# They are provided by Django, by third-parties, or by yourself.
|
||||
|
@ -635,6 +639,8 @@ ALLOWED_HOSTS = [
|
|||
"getgov-stable.app.cloud.gov",
|
||||
"getgov-staging.app.cloud.gov",
|
||||
"getgov-development.app.cloud.gov",
|
||||
"getgov-bob.app.cloud.gov",
|
||||
"getgov-meoward.app.cloud.gov",
|
||||
"getgov-backup.app.cloud.gov",
|
||||
"getgov-ky.app.cloud.gov",
|
||||
"getgov-es.app.cloud.gov",
|
||||
|
|
|
@ -20,18 +20,18 @@ from registrar.views.admin_views import (
|
|||
ExportDataUnmanagedDomains,
|
||||
)
|
||||
|
||||
from registrar.views.application import Step
|
||||
from registrar.views.domain_request import Step
|
||||
from registrar.views.utility import always_404
|
||||
from api.views import available, get_current_federal, get_current_full
|
||||
|
||||
|
||||
APPLICATION_NAMESPACE = views.ApplicationWizard.URL_NAMESPACE
|
||||
application_urls = [
|
||||
path("", views.ApplicationWizard.as_view(), name=""),
|
||||
DOMAIN_REQUEST_NAMESPACE = views.DomainRequestWizard.URL_NAMESPACE
|
||||
domain_request_urls = [
|
||||
path("", views.DomainRequestWizard.as_view(), name=""),
|
||||
path("finished/", views.Finished.as_view(), name="finished"),
|
||||
]
|
||||
|
||||
# dynamically generate the other application_urls
|
||||
# dynamically generate the other domain_request_urls
|
||||
for step, view in [
|
||||
# add/remove steps here
|
||||
(Step.ORGANIZATION_TYPE, views.OrganizationType),
|
||||
|
@ -50,7 +50,7 @@ for step, view in [
|
|||
(Step.REQUIREMENTS, views.Requirements),
|
||||
(Step.REVIEW, views.Review),
|
||||
]:
|
||||
application_urls.append(path(f"{step}/", view.as_view(), name=step))
|
||||
domain_request_urls.append(path(f"{step}/", view.as_view(), name=step))
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -101,28 +101,28 @@ urlpatterns = [
|
|||
),
|
||||
path("admin/", admin.site.urls),
|
||||
path(
|
||||
"application/<id>/edit/",
|
||||
views.ApplicationWizard.as_view(),
|
||||
name=views.ApplicationWizard.EDIT_URL_NAME,
|
||||
"domain-request/<id>/edit/",
|
||||
views.DomainRequestWizard.as_view(),
|
||||
name=views.DomainRequestWizard.EDIT_URL_NAME,
|
||||
),
|
||||
path(
|
||||
"application/<int:pk>",
|
||||
views.ApplicationStatus.as_view(),
|
||||
name="application-status",
|
||||
"domain-request/<int:pk>",
|
||||
views.DomainRequestStatus.as_view(),
|
||||
name="domain-request-status",
|
||||
),
|
||||
path(
|
||||
"application/<int:pk>/withdraw",
|
||||
views.ApplicationWithdrawConfirmation.as_view(),
|
||||
name="application-withdraw-confirmation",
|
||||
"domain-request/<int:pk>/withdraw",
|
||||
views.DomainRequestWithdrawConfirmation.as_view(),
|
||||
name="domain-request-withdraw-confirmation",
|
||||
),
|
||||
path(
|
||||
"application/<int:pk>/withdrawconfirmed",
|
||||
views.ApplicationWithdrawn.as_view(),
|
||||
name="application-withdrawn",
|
||||
"domain-request/<int:pk>/withdrawconfirmed",
|
||||
views.DomainRequestWithdrawn.as_view(),
|
||||
name="domain-request-withdrawn",
|
||||
),
|
||||
path("health", views.health, name="health"),
|
||||
path("openid/", include("djangooidc.urls")),
|
||||
path("request/", include((application_urls, APPLICATION_NAMESPACE))),
|
||||
path("request/", include((domain_request_urls, DOMAIN_REQUEST_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"),
|
||||
|
@ -184,9 +184,9 @@ urlpatterns = [
|
|||
name="invitation-delete",
|
||||
),
|
||||
path(
|
||||
"application/<int:pk>/delete",
|
||||
views.DomainApplicationDeleteView.as_view(http_method_names=["post"]),
|
||||
name="application-delete",
|
||||
"domain-request/<int:pk>/delete",
|
||||
views.DomainRequestDeleteView.as_view(http_method_names=["post"]),
|
||||
name="domain-request-delete",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/<int:user_pk>/delete",
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import logging
|
||||
import random
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models import (
|
||||
User,
|
||||
DomainApplication,
|
||||
DomainRequest,
|
||||
DraftDomain,
|
||||
Contact,
|
||||
Website,
|
||||
|
@ -14,9 +15,9 @@ fake = Faker()
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainApplicationFixture:
|
||||
class DomainRequestFixture:
|
||||
"""
|
||||
Load domain applications into the database.
|
||||
Load domain requests into the database.
|
||||
|
||||
Make sure this class' `load` method is called from `handle`
|
||||
in management/commands/load.py, then use `./manage.py load`
|
||||
|
@ -49,27 +50,27 @@ class DomainApplicationFixture:
|
|||
# },
|
||||
DA = [
|
||||
{
|
||||
"status": DomainApplication.ApplicationStatus.STARTED,
|
||||
"status": DomainRequest.DomainRequestStatus.STARTED,
|
||||
"organization_name": "Example - Finished but not submitted",
|
||||
},
|
||||
{
|
||||
"status": DomainApplication.ApplicationStatus.SUBMITTED,
|
||||
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
"organization_name": "Example - Submitted but pending investigation",
|
||||
},
|
||||
{
|
||||
"status": DomainApplication.ApplicationStatus.IN_REVIEW,
|
||||
"status": DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
"organization_name": "Example - In investigation",
|
||||
},
|
||||
{
|
||||
"status": DomainApplication.ApplicationStatus.IN_REVIEW,
|
||||
"status": DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
"organization_name": "Example - Approved",
|
||||
},
|
||||
{
|
||||
"status": DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
"status": DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
"organization_name": "Example - Withdrawn",
|
||||
},
|
||||
{
|
||||
"status": DomainApplication.ApplicationStatus.ACTION_NEEDED,
|
||||
"status": DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
"organization_name": "Example - Action needed",
|
||||
},
|
||||
{
|
||||
|
@ -94,7 +95,7 @@ class DomainApplicationFixture:
|
|||
return f"{fake.slug()}.gov"
|
||||
|
||||
@classmethod
|
||||
def _set_non_foreign_key_fields(cls, da: DomainApplication, app: dict):
|
||||
def _set_non_foreign_key_fields(cls, da: DomainRequest, app: dict):
|
||||
"""Helper method used by `load`."""
|
||||
da.status = app["status"] if "status" in app else "started"
|
||||
da.organization_type = app["organization_type"] if "organization_type" in app else "federal"
|
||||
|
@ -102,7 +103,7 @@ class DomainApplicationFixture:
|
|||
app["federal_agency"]
|
||||
if "federal_agency" in app
|
||||
# Random choice of agency for selects, used as placeholders for testing.
|
||||
else random.choice(DomainApplication.AGENCIES) # nosec
|
||||
else random.choice(DomainRequest.AGENCIES) # nosec
|
||||
)
|
||||
da.submission_date = fake.date()
|
||||
da.federal_type = (
|
||||
|
@ -121,7 +122,7 @@ class DomainApplicationFixture:
|
|||
da.is_policy_acknowledged = app["is_policy_acknowledged"] if "is_policy_acknowledged" in app else True
|
||||
|
||||
@classmethod
|
||||
def _set_foreign_key_fields(cls, da: DomainApplication, app: dict, user: User):
|
||||
def _set_foreign_key_fields(cls, da: DomainRequest, app: dict, user: User):
|
||||
"""Helper method used by `load`."""
|
||||
if not da.investigator:
|
||||
da.investigator = User.objects.get(username=user.username) if "investigator" in app else None
|
||||
|
@ -145,7 +146,7 @@ class DomainApplicationFixture:
|
|||
da.requested_domain = DraftDomain.objects.create(name=cls.fake_dot_gov())
|
||||
|
||||
@classmethod
|
||||
def _set_many_to_many_relations(cls, da: DomainApplication, app: dict):
|
||||
def _set_many_to_many_relations(cls, da: DomainRequest, app: dict):
|
||||
"""Helper method used by `load`."""
|
||||
if "other_contacts" in app:
|
||||
for contact in app["other_contacts"]:
|
||||
|
@ -176,19 +177,27 @@ class DomainApplicationFixture:
|
|||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Creates domain applications for each user in the database."""
|
||||
logger.info("Going to load %s domain applications" % len(cls.DA))
|
||||
"""Creates domain requests for each user in the database."""
|
||||
logger.info("Going to load %s domain requests" % len(cls.DA))
|
||||
try:
|
||||
users = list(User.objects.all()) # force evaluation to catch db errors
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
cls._create_domain_requests(users)
|
||||
|
||||
@classmethod
|
||||
def _create_domain_requests(cls, users):
|
||||
"""Creates DomainRequests given a list of users"""
|
||||
for user in users:
|
||||
logger.debug("Loading domain applications for %s" % user)
|
||||
logger.debug("Loading domain requests for %s" % user)
|
||||
for app in cls.DA:
|
||||
try:
|
||||
da, _ = DomainApplication.objects.get_or_create(
|
||||
da, _ = DomainRequest.objects.get_or_create(
|
||||
creator=user,
|
||||
organization_name=app["organization_name"],
|
||||
)
|
||||
|
@ -200,7 +209,7 @@ class DomainApplicationFixture:
|
|||
logger.warning(e)
|
||||
|
||||
|
||||
class DomainFixture(DomainApplicationFixture):
|
||||
class DomainFixture(DomainRequestFixture):
|
||||
"""Create one domain and permissions on it for each user."""
|
||||
|
||||
@classmethod
|
||||
|
@ -211,14 +220,30 @@ class DomainFixture(DomainApplicationFixture):
|
|||
logger.warning(e)
|
||||
return
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
# approve each user associated with `in review` status domains
|
||||
DomainFixture._approve_domain_requests(users)
|
||||
|
||||
@staticmethod
|
||||
def _approve_domain_requests(users):
|
||||
"""Approves all provided domain requests if they are in the state in_review"""
|
||||
for user in users:
|
||||
# approve one of each users in review status domains
|
||||
application = DomainApplication.objects.filter(
|
||||
creator=user, status=DomainApplication.ApplicationStatus.IN_REVIEW
|
||||
domain_request = DomainRequest.objects.filter(
|
||||
creator=user, status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
).last()
|
||||
logger.debug(f"Approving {application} for {user}")
|
||||
logger.debug(f"Approving {domain_request} for {user}")
|
||||
|
||||
# We don't want fixtures sending out real emails to
|
||||
# fake email addresses, so we just skip that and log it instead
|
||||
application.approve(send_email=False)
|
||||
application.save()
|
||||
|
||||
# All approvals require an investigator, so if there is none,
|
||||
# assign one.
|
||||
if domain_request.investigator is None:
|
||||
# All "users" in fixtures have admin perms per prior config.
|
||||
# No need to check for that.
|
||||
domain_request.investigator = random.choice(users) # nosec
|
||||
|
||||
domain_request.approve(send_email=False)
|
||||
domain_request.save()
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models import (
|
||||
User,
|
||||
|
@ -186,5 +187,12 @@ class UserFixture:
|
|||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.load_users(cls, cls.ADMINS, "full_access_group")
|
||||
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
# This is slightly different then bulk_create or bulk_update, in that
|
||||
# you still get the same behaviour of .save(), but those incremental
|
||||
# steps now do not need to close/reopen a db connection,
|
||||
# instead they share one.
|
||||
with transaction.atomic():
|
||||
cls.load_users(cls, cls.ADMINS, "full_access_group")
|
||||
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .application_wizard import *
|
||||
from .domain_request_wizard import *
|
||||
from .domain import (
|
||||
DomainAddUserForm,
|
||||
NameserverFormset,
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.core.validators import RegexValidator, MaxLengthValidator
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
|
||||
from registrar.models import Contact, DomainApplication, DraftDomain, Domain
|
||||
from registrar.models import Contact, DomainRequest, DraftDomain, Domain
|
||||
from registrar.templatetags.url_helpers import public_site_url
|
||||
from registrar.utility.enums import ValidationReturnType
|
||||
|
||||
|
@ -21,7 +21,7 @@ class RegistrarForm(forms.Form):
|
|||
"""
|
||||
A common set of methods and configuration.
|
||||
|
||||
The registrar's domain application is several pages of "steps".
|
||||
The registrar's domain request is several pages of "steps".
|
||||
Each step is an HTML form containing one or more Django "forms".
|
||||
|
||||
Subclass this class to create new forms.
|
||||
|
@ -29,11 +29,11 @@ class RegistrarForm(forms.Form):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("label_suffix", "")
|
||||
# save a reference to an application object
|
||||
self.application = kwargs.pop("application", None)
|
||||
# save a reference to a domain request object
|
||||
self.domain_request = kwargs.pop("domain_request", None)
|
||||
super(RegistrarForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_database(self, obj: DomainApplication | Contact):
|
||||
def to_database(self, obj: DomainRequest | Contact):
|
||||
"""
|
||||
Adds this form's cleaned data to `obj` and saves `obj`.
|
||||
|
||||
|
@ -46,7 +46,7 @@ class RegistrarForm(forms.Form):
|
|||
obj.save()
|
||||
|
||||
@classmethod
|
||||
def from_database(cls, obj: DomainApplication | Contact | None):
|
||||
def from_database(cls, obj: DomainRequest | Contact | None):
|
||||
"""Returns a dict of form field values gotten from `obj`."""
|
||||
if obj is None:
|
||||
return {}
|
||||
|
@ -61,8 +61,8 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# save a reference to an application object
|
||||
self.application = kwargs.pop("application", None)
|
||||
# save a reference to an domain_request object
|
||||
self.domain_request = kwargs.pop("domain_request", None)
|
||||
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
||||
# quick workaround to ensure that the HTML `required`
|
||||
# attribute shows up on required fields for any forms
|
||||
|
@ -85,7 +85,7 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
"""Code to run before an item in the formset is created in the database."""
|
||||
return cleaned
|
||||
|
||||
def to_database(self, obj: DomainApplication):
|
||||
def to_database(self, obj: DomainRequest):
|
||||
"""
|
||||
Adds this form's cleaned data to `obj` and saves `obj`.
|
||||
|
||||
|
@ -97,7 +97,7 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
|
||||
def _to_database(
|
||||
self,
|
||||
obj: DomainApplication,
|
||||
obj: DomainRequest,
|
||||
join: str,
|
||||
should_delete: Callable,
|
||||
pre_update: Callable,
|
||||
|
@ -137,14 +137,14 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
if should_delete(cleaned):
|
||||
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
||||
# Remove the specific relationship without deleting the object
|
||||
getattr(db_obj, related_name).remove(self.application)
|
||||
getattr(db_obj, related_name).remove(self.domain_request)
|
||||
else:
|
||||
# If there are no other relationships, delete the object
|
||||
db_obj.delete()
|
||||
else:
|
||||
if hasattr(db_obj, "has_more_than_one_join") and db_obj.has_more_than_one_join(related_name):
|
||||
# create a new db_obj and disconnect existing one
|
||||
getattr(db_obj, related_name).remove(self.application)
|
||||
getattr(db_obj, related_name).remove(self.domain_request)
|
||||
kwargs = pre_create(db_obj, cleaned)
|
||||
getattr(obj, join).create(**kwargs)
|
||||
else:
|
||||
|
@ -163,15 +163,15 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
return query.values()
|
||||
|
||||
@classmethod
|
||||
def from_database(cls, obj: DomainApplication, join: str, on_fetch: Callable):
|
||||
def from_database(cls, obj: DomainRequest, join: str, on_fetch: Callable):
|
||||
"""Returns a dict of form field values gotten from `obj`."""
|
||||
return on_fetch(getattr(obj, join).order_by("created_at")) # order matters
|
||||
|
||||
|
||||
class OrganizationTypeForm(RegistrarForm):
|
||||
organization_type = forms.ChoiceField(
|
||||
# use the long names in the application form
|
||||
choices=DomainApplication.OrganizationChoicesVerbose.choices,
|
||||
# use the long names in the domain request form
|
||||
choices=DomainRequest.OrganizationChoicesVerbose.choices,
|
||||
widget=forms.RadioSelect,
|
||||
error_messages={"required": "Select the type of organization you represent."},
|
||||
)
|
||||
|
@ -201,7 +201,7 @@ class TribalGovernmentForm(RegistrarForm):
|
|||
# into a link. There should be no user-facing input in the
|
||||
# HTML indicated here.
|
||||
mark_safe( # nosec
|
||||
"You can’t complete this application yet. "
|
||||
"You can’t complete this domain request yet. "
|
||||
"Only tribes recognized by the U.S. federal government "
|
||||
"or by a U.S. state government are eligible for .gov "
|
||||
'domains. Use our <a href="{}">contact form</a> to '
|
||||
|
@ -215,7 +215,7 @@ class TribalGovernmentForm(RegistrarForm):
|
|||
|
||||
class OrganizationFederalForm(RegistrarForm):
|
||||
federal_type = forms.ChoiceField(
|
||||
choices=DomainApplication.BranchChoices.choices,
|
||||
choices=DomainRequest.BranchChoices.choices,
|
||||
widget=forms.RadioSelect,
|
||||
error_messages={"required": ("Select the part of the federal government your organization is in.")},
|
||||
)
|
||||
|
@ -251,7 +251,7 @@ class OrganizationContactForm(RegistrarForm):
|
|||
# it is a federal agency. Use clean to check programatically
|
||||
# if it has been filled in when required.
|
||||
required=False,
|
||||
choices=[("", "--Select--")] + DomainApplication.AGENCY_CHOICES,
|
||||
choices=[("", "--Select--")] + DomainRequest.AGENCY_CHOICES,
|
||||
)
|
||||
organization_name = forms.CharField(
|
||||
label="Organization name",
|
||||
|
@ -271,7 +271,7 @@ class OrganizationContactForm(RegistrarForm):
|
|||
)
|
||||
state_territory = forms.ChoiceField(
|
||||
label="State, territory, or military post",
|
||||
choices=[("", "--Select--")] + DomainApplication.StateTerritoryChoices.choices,
|
||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
||||
error_messages={
|
||||
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||
},
|
||||
|
@ -294,16 +294,16 @@ class OrganizationContactForm(RegistrarForm):
|
|||
def clean_federal_agency(self):
|
||||
"""Require something to be selected when this is a federal agency."""
|
||||
federal_agency = self.cleaned_data.get("federal_agency", None)
|
||||
# need the application object to know if this is federal
|
||||
if self.application is None:
|
||||
# hmm, no saved application object?, default require the agency
|
||||
# need the domain request object to know if this is federal
|
||||
if self.domain_request is None:
|
||||
# hmm, no saved domain request object?, default require the agency
|
||||
if not federal_agency:
|
||||
# no answer was selected
|
||||
raise forms.ValidationError(
|
||||
"Select the federal agency your organization is in.",
|
||||
code="required",
|
||||
)
|
||||
if self.application.is_federal():
|
||||
if self.domain_request.is_federal():
|
||||
if not federal_agency:
|
||||
# no answer was selected
|
||||
raise forms.ValidationError(
|
||||
|
@ -390,7 +390,7 @@ class BaseCurrentSitesFormSet(RegistrarFormSet):
|
|||
website = cleaned.get("website", "")
|
||||
return website.strip() == ""
|
||||
|
||||
def to_database(self, obj: DomainApplication):
|
||||
def to_database(self, obj: DomainRequest):
|
||||
# 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)
|
||||
|
@ -444,7 +444,7 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
|
|||
else:
|
||||
return {}
|
||||
|
||||
def to_database(self, obj: DomainApplication):
|
||||
def to_database(self, obj: DomainRequest):
|
||||
# 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)
|
||||
|
@ -530,7 +530,7 @@ class YourContactForm(RegistrarForm):
|
|||
if not self.is_valid():
|
||||
return
|
||||
contact = getattr(obj, "submitter", None)
|
||||
if contact is not None and not contact.has_more_than_one_join("submitted_applications"):
|
||||
if contact is not None and not contact.has_more_than_one_join("submitted_domain_requests"):
|
||||
# if contact exists in the database and is not joined to other entities
|
||||
super().to_database(contact)
|
||||
else:
|
||||
|
@ -578,13 +578,13 @@ 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():
|
||||
# set the initial value based on attributes of domain request
|
||||
if self.domain_request and self.domain_request.has_other_contacts():
|
||||
initial_value = True
|
||||
elif self.application and self.application.has_rationale():
|
||||
elif self.domain_request and self.domain_request.has_rationale():
|
||||
initial_value = False
|
||||
else:
|
||||
# No pre-selection for new applications
|
||||
# No pre-selection for new domain requests
|
||||
initial_value = None
|
||||
|
||||
self.fields["has_other_contacts"] = forms.TypedChoiceField(
|
||||
|
@ -687,7 +687,7 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
|
|||
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
|
||||
to delete an other_contact from a domain request, those db relationships must be
|
||||
tested and handled.
|
||||
"""
|
||||
|
||||
|
@ -701,7 +701,7 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
|
|||
Override __init__ for RegistrarFormSet.
|
||||
"""
|
||||
self.formset_data_marked_for_deletion = False
|
||||
self.application = kwargs.pop("application", None)
|
||||
self.domain_request = kwargs.pop("domain_request", None)
|
||||
super(RegistrarFormSet, self).__init__(*args, **kwargs)
|
||||
# quick workaround to ensure that the HTML `required`
|
||||
# attribute shows up on required fields for the first form
|
||||
|
@ -722,7 +722,7 @@ class BaseOtherContactsFormSet(RegistrarFormSet):
|
|||
cleaned.pop("DELETE")
|
||||
return cleaned
|
||||
|
||||
def to_database(self, obj: DomainApplication):
|
||||
def to_database(self, obj: DomainRequest):
|
||||
self._to_database(obj, self.JOIN, self.should_delete, self.pre_update, self.pre_create)
|
||||
|
||||
@classmethod
|
|
@ -0,0 +1,105 @@
|
|||
"""Generates current-metadata.csv then uploads to S3 + sends email"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pyzipper
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.conf import settings
|
||||
from registrar.utility import csv_export
|
||||
from registrar.utility.s3_bucket import S3ClientHelper
|
||||
from ...utility.email import send_templated_email
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Generates and uploads a domain-metadata.csv file to our S3 bucket "
|
||||
"which is based off of all existing Domains."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add our two filename arguments."""
|
||||
parser.add_argument("--directory", default="migrationdata", help="Desired directory")
|
||||
parser.add_argument(
|
||||
"--checkpath",
|
||||
default=True,
|
||||
help="Flag that determines if we do a check for os.path.exists. Used for test cases",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
"""Grabs the directory then creates domain-metadata.csv in that directory"""
|
||||
file_name = "domain-metadata.csv"
|
||||
# Ensures a slash is added
|
||||
directory = os.path.join(options.get("directory"), "")
|
||||
check_path = options.get("checkpath")
|
||||
|
||||
logger.info("Generating report...")
|
||||
try:
|
||||
self.email_current_metadata_report(directory, file_name, check_path)
|
||||
except Exception as err:
|
||||
# TODO - #1317: Notify operations when auto report generation fails
|
||||
raise err
|
||||
else:
|
||||
logger.info(f"Success! Created {file_name} and successfully sent out an email!")
|
||||
|
||||
def email_current_metadata_report(self, directory, file_name, check_path):
|
||||
"""Creates a current-metadata.csv file under the specified directory,
|
||||
then uploads it to a AWS S3 bucket. This is done for resiliency
|
||||
reasons in the event our application goes down and/or the email
|
||||
cannot send -- we'll still be able to grab info from the S3
|
||||
instance"""
|
||||
s3_client = S3ClientHelper()
|
||||
file_path = os.path.join(directory, file_name)
|
||||
|
||||
# Generate a file locally for upload
|
||||
with open(file_path, "w") as file:
|
||||
csv_export.export_data_type_to_csv(file)
|
||||
|
||||
if check_path and not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||
|
||||
s3_client.upload_file(file_path, file_name)
|
||||
|
||||
# Set zip file name
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.zip"
|
||||
|
||||
# Pre-set zip file name
|
||||
encrypted_metadata_output = current_filename
|
||||
|
||||
# Set context for the subject
|
||||
current_date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Encrypt the metadata
|
||||
encrypted_metadata_in_bytes = self._encrypt_metadata(
|
||||
s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA)
|
||||
)
|
||||
|
||||
# Send the metadata file that is zipped
|
||||
send_templated_email(
|
||||
template_name="emails/metadata_body.txt",
|
||||
subject_template_name="emails/metadata_subject.txt",
|
||||
to_address=settings.DEFAULT_FROM_EMAIL,
|
||||
context={"current_date_str": current_date_str},
|
||||
attachment_file=encrypted_metadata_in_bytes,
|
||||
)
|
||||
|
||||
def _encrypt_metadata(self, input_file, output_file, password):
|
||||
"""Helper function for encrypting the attachment file"""
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.csv"
|
||||
# Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster
|
||||
# We could also use compression=pyzipper.ZIP_LZMA if we are looking for smaller file size
|
||||
with pyzipper.AESZipFile(
|
||||
output_file, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES
|
||||
) as f_out:
|
||||
f_out.setpassword(password)
|
||||
f_out.writestr(current_filename, input_file)
|
||||
with open(output_file, "rb") as file_data:
|
||||
attachment_in_bytes = file_data.read()
|
||||
return attachment_in_bytes
|
|
@ -5,7 +5,7 @@ from auditlog.context import disable_auditlog # type: ignore
|
|||
|
||||
|
||||
from registrar.fixtures_users import UserFixture
|
||||
from registrar.fixtures_applications import DomainApplicationFixture, DomainFixture
|
||||
from registrar.fixtures_domain_requests import DomainRequestFixture, DomainFixture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -16,6 +16,6 @@ class Command(BaseCommand):
|
|||
# https://github.com/jazzband/django-auditlog/issues/17
|
||||
with disable_auditlog():
|
||||
UserFixture.load()
|
||||
DomainApplicationFixture.load()
|
||||
DomainRequestFixture.load()
|
||||
DomainFixture.load()
|
||||
logger.info("All fixtures loaded.")
|
||||
|
|
|
@ -15,7 +15,7 @@ from registrar.management.commands.utility.terminal_helper import (
|
|||
TerminalHelper,
|
||||
)
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.domain_application import DomainApplication
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.user import User
|
||||
|
||||
|
@ -817,9 +817,9 @@ class Command(BaseCommand):
|
|||
raise Exception(f"Domain {existing_domain} wants to be added" "but doesn't exist in the DB")
|
||||
invitation.save()
|
||||
|
||||
valid_org_choices = [(name, value) for name, value in DomainApplication.OrganizationChoices.choices]
|
||||
valid_fed_choices = [value for name, value in DomainApplication.BranchChoices.choices]
|
||||
valid_agency_choices = DomainApplication.AGENCIES
|
||||
valid_org_choices = [(name, value) for name, value in DomainRequest.OrganizationChoices.choices]
|
||||
valid_fed_choices = [value for name, value in DomainRequest.BranchChoices.choices]
|
||||
valid_agency_choices = DomainRequest.AGENCIES
|
||||
# ======================================================
|
||||
# ================= DOMAIN INFORMATION =================
|
||||
logger.info(
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-12 16:50
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0072_alter_publiccontact_fax_alter_publiccontact_voice"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="approved_domain",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="The approved domain",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="domain_request",
|
||||
to="registrar.domain",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="creator",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain_requests_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="investigator",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="domain_requests_investigating",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="contact_domain_requests", to="registrar.contact", verbose_name="contacts"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="requested_domain",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="The requested domain",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain_request",
|
||||
to="registrar.draftdomain",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="submitter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="submitted_domain_requests",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="domain_application",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="Associated domain request",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="DomainRequest_info",
|
||||
to="registrar.domainapplication",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
verbose_name="contacts",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="submitter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="submitted_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-12 16:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0073_alter_domainapplication_approved_domain_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="DomainApplication",
|
||||
new_name="DomainRequest",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="domaininformation",
|
||||
old_name="domain_application",
|
||||
new_name="domain_request",
|
||||
),
|
||||
]
|
37
src/registrar/migrations/0075_create_groups_v08.py
Normal file
37
src/registrar/migrations/0075_create_groups_v08.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0035 (which populates ContentType and Permissions)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0074_rename_domainapplication_domainrequest_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-13 21:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0075_create_groups_v08"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="current_websites",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="current+", to="registrar.website", verbose_name="Current websites"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests",
|
||||
to="registrar.contact",
|
||||
verbose_name="Other employees",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
verbose_name="Other employees",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,6 @@
|
|||
from auditlog.registry import auditlog # type: ignore
|
||||
from .contact import Contact
|
||||
from .domain_application import DomainApplication
|
||||
from .domain_request import DomainRequest
|
||||
from .domain_information import DomainInformation
|
||||
from .domain import Domain
|
||||
from .draft_domain import DraftDomain
|
||||
|
@ -17,7 +17,7 @@ from .verified_by_staff import VerifiedByStaff
|
|||
|
||||
__all__ = [
|
||||
"Contact",
|
||||
"DomainApplication",
|
||||
"DomainRequest",
|
||||
"DomainInformation",
|
||||
"Domain",
|
||||
"DraftDomain",
|
||||
|
@ -34,7 +34,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
auditlog.register(Contact)
|
||||
auditlog.register(DomainApplication)
|
||||
auditlog.register(DomainRequest)
|
||||
auditlog.register(Domain)
|
||||
auditlog.register(DraftDomain)
|
||||
auditlog.register(DomainInvitation)
|
||||
|
|
|
@ -897,8 +897,8 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
def security_contact(self, contact: PublicContact):
|
||||
"""makes the contact in the registry,
|
||||
for security the public contact should have the org or registrant information
|
||||
from domain information (not domain application)
|
||||
and should have the security email from DomainApplication"""
|
||||
from domain information (not domain request)
|
||||
and should have the security email from DomainRequest"""
|
||||
logger.info("making security contact in registry")
|
||||
self._set_singleton_contact(contact, expectedType=contact.ContactTypeChoices.SECURITY)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
from django.db import transaction
|
||||
|
||||
from registrar.models.utility.domain_helper import DomainHelper
|
||||
from .domain_application import DomainApplication
|
||||
from .domain_request import DomainRequest
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
import logging
|
||||
|
@ -15,22 +15,22 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class DomainInformation(TimeStampedModel):
|
||||
"""A registrant's domain information for that domain, exported from
|
||||
DomainApplication. We use these field from DomainApplication with few exceptions
|
||||
DomainRequest. We use these field from DomainRequest with few exceptions
|
||||
which are 'removed' via pop at the bottom of this file. Most of design for domain
|
||||
management's user information are based on application, but we cannot change
|
||||
the application once approved, so copying them that way we can make changes
|
||||
after its approved. Most fields here are copied from Application."""
|
||||
management's user information are based on domain_request, but we cannot change
|
||||
the domain request once approved, so copying them that way we can make changes
|
||||
after its approved. Most fields here are copied from DomainRequest."""
|
||||
|
||||
StateTerritoryChoices = DomainApplication.StateTerritoryChoices
|
||||
StateTerritoryChoices = DomainRequest.StateTerritoryChoices
|
||||
|
||||
# use the short names in Django admin
|
||||
OrganizationChoices = DomainApplication.OrganizationChoices
|
||||
OrganizationChoices = DomainRequest.OrganizationChoices
|
||||
|
||||
BranchChoices = DomainApplication.BranchChoices
|
||||
BranchChoices = DomainRequest.BranchChoices
|
||||
|
||||
AGENCY_CHOICES = DomainApplication.AGENCY_CHOICES
|
||||
AGENCY_CHOICES = DomainRequest.AGENCY_CHOICES
|
||||
|
||||
# This is the application user who created this application. The contact
|
||||
# This is the domain request user who created this domain request. The contact
|
||||
# information that they gave is in the `submitter` field
|
||||
creator = models.ForeignKey(
|
||||
"registrar.User",
|
||||
|
@ -38,13 +38,13 @@ class DomainInformation(TimeStampedModel):
|
|||
related_name="information_created",
|
||||
)
|
||||
|
||||
domain_application = models.OneToOneField(
|
||||
"registrar.DomainApplication",
|
||||
domain_request = models.OneToOneField(
|
||||
"registrar.DomainRequest",
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="domainapplication_info",
|
||||
help_text="Associated domain application",
|
||||
related_name="DomainRequest_info",
|
||||
help_text="Associated domain request",
|
||||
unique=True,
|
||||
)
|
||||
|
||||
|
@ -163,13 +163,13 @@ class DomainInformation(TimeStampedModel):
|
|||
help_text="Domain to which this information belongs",
|
||||
)
|
||||
|
||||
# This is the contact information provided by the applicant. The
|
||||
# application user who created it is in the `creator` field.
|
||||
# This is the contact information provided by the domain requestor. The
|
||||
# user who created the domain request is in the `creator` field.
|
||||
submitter = models.ForeignKey(
|
||||
"registrar.Contact",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="submitted_applications_information",
|
||||
related_name="submitted_domain_requests_information",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
|
@ -182,8 +182,8 @@ class DomainInformation(TimeStampedModel):
|
|||
other_contacts = models.ManyToManyField(
|
||||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_applications_information",
|
||||
verbose_name="contacts",
|
||||
related_name="contact_domain_requests_information",
|
||||
verbose_name="Other employees",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
@ -220,25 +220,25 @@ class DomainInformation(TimeStampedModel):
|
|||
return ""
|
||||
|
||||
@classmethod
|
||||
def create_from_da(cls, domain_application: DomainApplication, domain=None):
|
||||
"""Takes in a DomainApplication and converts it into DomainInformation"""
|
||||
def create_from_da(cls, domain_request: DomainRequest, domain=None):
|
||||
"""Takes in a DomainRequest and converts it into DomainInformation"""
|
||||
|
||||
# Throw an error if we get None - we can't create something from nothing
|
||||
if domain_application is None:
|
||||
raise ValueError("The provided DomainApplication is None")
|
||||
if domain_request is None:
|
||||
raise ValueError("The provided DomainRequest is None")
|
||||
|
||||
# Throw an error if the da doesn't have an id
|
||||
if not hasattr(domain_application, "id"):
|
||||
raise ValueError("The provided DomainApplication has no id")
|
||||
if not hasattr(domain_request, "id"):
|
||||
raise ValueError("The provided DomainRequest has no id")
|
||||
|
||||
# check if we have a record that corresponds with the domain
|
||||
# application, if so short circuit the create
|
||||
existing_domain_info = cls.objects.filter(domain_application__id=domain_application.id).first()
|
||||
# domain_request, if so short circuit the create
|
||||
existing_domain_info = cls.objects.filter(domain_request__id=domain_request.id).first()
|
||||
if existing_domain_info:
|
||||
return existing_domain_info
|
||||
|
||||
# Get the fields that exist on both DomainApplication and DomainInformation
|
||||
common_fields = DomainHelper.get_common_fields(DomainApplication, DomainInformation)
|
||||
# Get the fields that exist on both DomainRequest and DomainInformation
|
||||
common_fields = DomainHelper.get_common_fields(DomainRequest, DomainInformation)
|
||||
|
||||
# Get a list of all many_to_many relations on DomainInformation (needs to be saved differently)
|
||||
info_many_to_many_fields = DomainInformation._get_many_to_many_fields()
|
||||
|
@ -249,11 +249,11 @@ class DomainInformation(TimeStampedModel):
|
|||
for field in common_fields:
|
||||
# If the field isn't many_to_many, populate the da_dict.
|
||||
# If it is, populate da_many_to_many_dict as we need to save this later.
|
||||
if hasattr(domain_application, field):
|
||||
if hasattr(domain_request, field):
|
||||
if field not in info_many_to_many_fields:
|
||||
da_dict[field] = getattr(domain_application, field)
|
||||
da_dict[field] = getattr(domain_request, field)
|
||||
else:
|
||||
da_many_to_many_dict[field] = getattr(domain_application, field).all()
|
||||
da_many_to_many_dict[field] = getattr(domain_request, field).all()
|
||||
|
||||
# This will not happen in normal code flow, but having some redundancy doesn't hurt.
|
||||
# da_dict should not have "id" under any circumstances.
|
||||
|
@ -266,8 +266,8 @@ class DomainInformation(TimeStampedModel):
|
|||
# Create a placeholder DomainInformation object
|
||||
domain_info = DomainInformation(**da_dict)
|
||||
|
||||
# Add the domain_application and domain fields
|
||||
domain_info.domain_application = domain_application
|
||||
# Add the domain_request and domain fields
|
||||
domain_info.domain_request = domain_request
|
||||
if domain:
|
||||
domain_info.domain = domain
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.db import models
|
|||
from django_fsm import FSMField, transition # type: ignore
|
||||
from django.utils import timezone
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.utility.errors import FSMApplicationError, FSMErrorCodes
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
|
@ -17,11 +18,11 @@ from itertools import chain
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainApplication(TimeStampedModel):
|
||||
"""A registrant's application for a new domain."""
|
||||
class DomainRequest(TimeStampedModel):
|
||||
"""A registrant's domain request for a new domain."""
|
||||
|
||||
# Constants for choice fields
|
||||
class ApplicationStatus(models.TextChoices):
|
||||
class DomainRequestStatus(models.TextChoices):
|
||||
STARTED = "started", "Started"
|
||||
SUBMITTED = "submitted", "Submitted"
|
||||
IN_REVIEW = "in review", "In review"
|
||||
|
@ -115,7 +116,7 @@ class DomainApplication(TimeStampedModel):
|
|||
class OrganizationChoicesVerbose(models.TextChoices):
|
||||
"""
|
||||
Secondary organization choices
|
||||
For use in the application form and on the templates
|
||||
For use in the domain request form and on the templates
|
||||
Keys need to match OrganizationChoices
|
||||
"""
|
||||
|
||||
|
@ -366,10 +367,10 @@ class DomainApplication(TimeStampedModel):
|
|||
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
|
||||
OTHER = "other", "Other/Unspecified"
|
||||
|
||||
# #### Internal fields about the application #####
|
||||
# #### Internal fields about the domain request #####
|
||||
status = FSMField(
|
||||
choices=ApplicationStatus.choices, # possible states as an array of constants
|
||||
default=ApplicationStatus.STARTED, # sensible default
|
||||
choices=DomainRequestStatus.choices, # possible states as an array of constants
|
||||
default=DomainRequestStatus.STARTED, # sensible default
|
||||
protected=False, # can change state directly, particularly in Django admin
|
||||
)
|
||||
|
||||
|
@ -379,12 +380,12 @@ class DomainApplication(TimeStampedModel):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
# This is the application user who created this application. The contact
|
||||
# This is the domain request user who created this domain request. The contact
|
||||
# information that they gave is in the `submitter` field
|
||||
creator = models.ForeignKey(
|
||||
"registrar.User",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="applications_created",
|
||||
related_name="domain_requests_created",
|
||||
)
|
||||
|
||||
investigator = models.ForeignKey(
|
||||
|
@ -392,7 +393,7 @@ class DomainApplication(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="applications_investigating",
|
||||
related_name="domain_requests_investigating",
|
||||
)
|
||||
|
||||
# ##### data fields from the initial form #####
|
||||
|
@ -499,12 +500,12 @@ class DomainApplication(TimeStampedModel):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
# "+" means no reverse relation to lookup applications from Website
|
||||
# "+" means no reverse relation to lookup domain requests from Website
|
||||
current_websites = models.ManyToManyField(
|
||||
"registrar.Website",
|
||||
blank=True,
|
||||
related_name="current+",
|
||||
verbose_name="websites",
|
||||
verbose_name="Current websites",
|
||||
)
|
||||
|
||||
approved_domain = models.OneToOneField(
|
||||
|
@ -512,7 +513,7 @@ class DomainApplication(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
help_text="The approved domain",
|
||||
related_name="domain_application",
|
||||
related_name="domain_request",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
|
@ -521,7 +522,7 @@ class DomainApplication(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
help_text="The requested domain",
|
||||
related_name="domain_application",
|
||||
related_name="domain_request",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
alternative_domains = models.ManyToManyField(
|
||||
|
@ -530,13 +531,13 @@ class DomainApplication(TimeStampedModel):
|
|||
related_name="alternatives+",
|
||||
)
|
||||
|
||||
# This is the contact information provided by the applicant. The
|
||||
# application user who created it is in the `creator` field.
|
||||
# This is the contact information provided by the domain requestor. The
|
||||
# user who created the domain request is in the `creator` field.
|
||||
submitter = models.ForeignKey(
|
||||
"registrar.Contact",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="submitted_applications",
|
||||
related_name="submitted_domain_requests",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
|
@ -549,8 +550,8 @@ class DomainApplication(TimeStampedModel):
|
|||
other_contacts = models.ManyToManyField(
|
||||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_applications",
|
||||
verbose_name="contacts",
|
||||
related_name="contact_domain_requests",
|
||||
verbose_name="Other employees",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
@ -571,7 +572,7 @@ class DomainApplication(TimeStampedModel):
|
|||
help_text="Acknowledged .gov acceptable use policy",
|
||||
)
|
||||
|
||||
# submission date records when application is submitted
|
||||
# submission date records when domain request is submitted
|
||||
submission_date = models.DateField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -590,7 +591,7 @@ class DomainApplication(TimeStampedModel):
|
|||
if self.requested_domain and self.requested_domain.name:
|
||||
return self.requested_domain.name
|
||||
else:
|
||||
return f"{self.status} application created by {self.creator}"
|
||||
return f"{self.status} domain request created by {self.creator}"
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
@ -638,25 +639,33 @@ class DomainApplication(TimeStampedModel):
|
|||
email_template,
|
||||
email_template_subject,
|
||||
self.submitter.email,
|
||||
context={"application": self},
|
||||
context={"domain_request": self},
|
||||
bcc_address=bcc_address,
|
||||
)
|
||||
logger.info(f"The {new_status} email sent to: {self.submitter.email}")
|
||||
except EmailSendingError:
|
||||
logger.warning("Failed to send confirmation email", exc_info=True)
|
||||
|
||||
def investigator_exists_and_is_staff(self):
|
||||
"""Checks if the current investigator is in a valid state for a state transition"""
|
||||
is_valid = True
|
||||
# Check if an investigator is assigned. No approval is possible without one.
|
||||
if self.investigator is None or not self.investigator.is_staff:
|
||||
is_valid = False
|
||||
return is_valid
|
||||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[
|
||||
ApplicationStatus.STARTED,
|
||||
ApplicationStatus.IN_REVIEW,
|
||||
ApplicationStatus.ACTION_NEEDED,
|
||||
ApplicationStatus.WITHDRAWN,
|
||||
DomainRequestStatus.STARTED,
|
||||
DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequestStatus.WITHDRAWN,
|
||||
],
|
||||
target=ApplicationStatus.SUBMITTED,
|
||||
target=DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
def submit(self):
|
||||
"""Submit an application that is started.
|
||||
"""Submit an domain request that is started.
|
||||
|
||||
As a side effect, an email notification is sent."""
|
||||
|
||||
|
@ -664,10 +673,7 @@ class DomainApplication(TimeStampedModel):
|
|||
# can raise more informative exceptions
|
||||
|
||||
# requested_domain could be None here
|
||||
if not hasattr(self, "requested_domain"):
|
||||
raise ValueError("Requested domain is missing.")
|
||||
|
||||
if self.requested_domain is None:
|
||||
if not hasattr(self, "requested_domain") or self.requested_domain is None:
|
||||
raise ValueError("Requested domain is missing.")
|
||||
|
||||
DraftDomain = apps.get_model("registrar.DraftDomain")
|
||||
|
@ -679,7 +685,7 @@ class DomainApplication(TimeStampedModel):
|
|||
self.save()
|
||||
|
||||
# Limit email notifications to transitions from Started and Withdrawn
|
||||
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
|
||||
limited_statuses = [self.DomainRequestStatus.STARTED, self.DomainRequestStatus.WITHDRAWN]
|
||||
|
||||
bcc_address = ""
|
||||
if settings.IS_PRODUCTION:
|
||||
|
@ -697,17 +703,17 @@ class DomainApplication(TimeStampedModel):
|
|||
@transition(
|
||||
field="status",
|
||||
source=[
|
||||
ApplicationStatus.SUBMITTED,
|
||||
ApplicationStatus.ACTION_NEEDED,
|
||||
ApplicationStatus.APPROVED,
|
||||
ApplicationStatus.REJECTED,
|
||||
ApplicationStatus.INELIGIBLE,
|
||||
DomainRequestStatus.SUBMITTED,
|
||||
DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequestStatus.APPROVED,
|
||||
DomainRequestStatus.REJECTED,
|
||||
DomainRequestStatus.INELIGIBLE,
|
||||
],
|
||||
target=ApplicationStatus.IN_REVIEW,
|
||||
conditions=[domain_is_not_active],
|
||||
target=DomainRequestStatus.IN_REVIEW,
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def in_review(self):
|
||||
"""Investigate an application that has been submitted.
|
||||
"""Investigate an domain request that has been submitted.
|
||||
|
||||
This action is logged.
|
||||
|
||||
|
@ -716,13 +722,13 @@ class DomainApplication(TimeStampedModel):
|
|||
As side effects this will delete the domain and domain_information
|
||||
(will cascade) when they exist."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
if self.status == self.DomainRequestStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("in_review")
|
||||
|
||||
if self.status == self.ApplicationStatus.REJECTED:
|
||||
if self.status == self.DomainRequestStatus.REJECTED:
|
||||
self.rejection_reason = None
|
||||
|
||||
literal = DomainApplication.ApplicationStatus.IN_REVIEW
|
||||
literal = DomainRequest.DomainRequestStatus.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}'")
|
||||
|
@ -730,16 +736,16 @@ class DomainApplication(TimeStampedModel):
|
|||
@transition(
|
||||
field="status",
|
||||
source=[
|
||||
ApplicationStatus.IN_REVIEW,
|
||||
ApplicationStatus.APPROVED,
|
||||
ApplicationStatus.REJECTED,
|
||||
ApplicationStatus.INELIGIBLE,
|
||||
DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequestStatus.APPROVED,
|
||||
DomainRequestStatus.REJECTED,
|
||||
DomainRequestStatus.INELIGIBLE,
|
||||
],
|
||||
target=ApplicationStatus.ACTION_NEEDED,
|
||||
conditions=[domain_is_not_active],
|
||||
target=DomainRequestStatus.ACTION_NEEDED,
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def action_needed(self):
|
||||
"""Send back an application that is under investigation or rejected.
|
||||
"""Send back an domain request that is under investigation or rejected.
|
||||
|
||||
This action is logged.
|
||||
|
||||
|
@ -748,13 +754,13 @@ class DomainApplication(TimeStampedModel):
|
|||
As side effects this will delete the domain and domain_information
|
||||
(will cascade) when they exist."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
if self.status == self.DomainRequestStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||
|
||||
if self.status == self.ApplicationStatus.REJECTED:
|
||||
if self.status == self.DomainRequestStatus.REJECTED:
|
||||
self.rejection_reason = None
|
||||
|
||||
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
||||
literal = DomainRequest.DomainRequestStatus.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}'")
|
||||
|
@ -762,33 +768,38 @@ class DomainApplication(TimeStampedModel):
|
|||
@transition(
|
||||
field="status",
|
||||
source=[
|
||||
ApplicationStatus.SUBMITTED,
|
||||
ApplicationStatus.IN_REVIEW,
|
||||
ApplicationStatus.ACTION_NEEDED,
|
||||
ApplicationStatus.REJECTED,
|
||||
DomainRequestStatus.SUBMITTED,
|
||||
DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequestStatus.REJECTED,
|
||||
],
|
||||
target=ApplicationStatus.APPROVED,
|
||||
target=DomainRequestStatus.APPROVED,
|
||||
conditions=[investigator_exists_and_is_staff],
|
||||
)
|
||||
def approve(self, send_email=True):
|
||||
"""Approve an application that has been submitted.
|
||||
"""Approve an domain request that has been submitted.
|
||||
|
||||
This action cleans up the rejection status if moving away from rejected.
|
||||
|
||||
This has substantial side-effects because it creates another database
|
||||
object for the approved Domain and makes the user who created the
|
||||
application into an admin on that domain. It also triggers an email
|
||||
domain request into an admin on that domain. It also triggers an email
|
||||
notification."""
|
||||
|
||||
# create the domain
|
||||
Domain = apps.get_model("registrar.Domain")
|
||||
|
||||
# == Check that the domain_request is valid == #
|
||||
if Domain.objects.filter(name=self.requested_domain.name).exists():
|
||||
raise ValueError("Cannot approve. Requested domain is already in use.")
|
||||
raise FSMApplicationError(code=FSMErrorCodes.APPROVE_DOMAIN_IN_USE)
|
||||
|
||||
# == Create the domain and related components == #
|
||||
created_domain = Domain.objects.create(name=self.requested_domain.name)
|
||||
self.approved_domain = created_domain
|
||||
|
||||
# copy the information from domainapplication into domaininformation
|
||||
# copy the information from DomainRequest into domaininformation
|
||||
DomainInformation = apps.get_model("registrar.DomainInformation")
|
||||
DomainInformation.create_from_da(domain_application=self, domain=created_domain)
|
||||
DomainInformation.create_from_da(domain_request=self, domain=created_domain)
|
||||
|
||||
# create the permission for the user
|
||||
UserDomainRole = apps.get_model("registrar.UserDomainRole")
|
||||
|
@ -796,11 +807,12 @@ class DomainApplication(TimeStampedModel):
|
|||
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
if self.status == self.ApplicationStatus.REJECTED:
|
||||
if self.status == self.DomainRequestStatus.REJECTED:
|
||||
self.rejection_reason = None
|
||||
|
||||
# == Send out an email == #
|
||||
self._send_status_update_email(
|
||||
"application approved",
|
||||
"domain request approved",
|
||||
"emails/status_change_approved.txt",
|
||||
"emails/status_change_approved_subject.txt",
|
||||
send_email,
|
||||
|
@ -808,11 +820,11 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED],
|
||||
target=ApplicationStatus.WITHDRAWN,
|
||||
source=[DomainRequestStatus.SUBMITTED, DomainRequestStatus.IN_REVIEW, DomainRequestStatus.ACTION_NEEDED],
|
||||
target=DomainRequestStatus.WITHDRAWN,
|
||||
)
|
||||
def withdraw(self):
|
||||
"""Withdraw an application that has been submitted."""
|
||||
"""Withdraw an domain request that has been submitted."""
|
||||
|
||||
self._send_status_update_email(
|
||||
"withdraw",
|
||||
|
@ -822,17 +834,17 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.APPROVED],
|
||||
target=ApplicationStatus.REJECTED,
|
||||
conditions=[domain_is_not_active],
|
||||
source=[DomainRequestStatus.IN_REVIEW, DomainRequestStatus.ACTION_NEEDED, DomainRequestStatus.APPROVED],
|
||||
target=DomainRequestStatus.REJECTED,
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def reject(self):
|
||||
"""Reject an application that has been submitted.
|
||||
"""Reject an domain request that has been submitted.
|
||||
|
||||
As side effects this will delete the domain and domain_information
|
||||
(will cascade), and send an email notification."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
if self.status == self.DomainRequestStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("reject")
|
||||
|
||||
self._send_status_update_email(
|
||||
|
@ -844,24 +856,24 @@ class DomainApplication(TimeStampedModel):
|
|||
@transition(
|
||||
field="status",
|
||||
source=[
|
||||
ApplicationStatus.IN_REVIEW,
|
||||
ApplicationStatus.ACTION_NEEDED,
|
||||
ApplicationStatus.APPROVED,
|
||||
ApplicationStatus.REJECTED,
|
||||
DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequestStatus.APPROVED,
|
||||
DomainRequestStatus.REJECTED,
|
||||
],
|
||||
target=ApplicationStatus.INELIGIBLE,
|
||||
conditions=[domain_is_not_active],
|
||||
target=DomainRequestStatus.INELIGIBLE,
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def reject_with_prejudice(self):
|
||||
"""The applicant is a bad actor, reject with prejudice.
|
||||
|
||||
No email As a side effect, but we block the applicant from editing
|
||||
any existing domains/applications and from submitting new aplications.
|
||||
any existing domains/domain requests and from submitting new aplications.
|
||||
We do this by setting an ineligible status on the user, which the
|
||||
permissions classes test against. This will also delete the domain
|
||||
and domain_information (will cascade) when they exist."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
if self.status == self.DomainRequestStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||
|
||||
self.creator.restrict_user()
|
||||
|
@ -869,18 +881,18 @@ class DomainApplication(TimeStampedModel):
|
|||
# ## Form policies ###
|
||||
#
|
||||
# These methods control what questions need to be answered by applicants
|
||||
# during the application flow. They are policies about the application so
|
||||
# during the domain request flow. They are policies about the domain request so
|
||||
# they appear here.
|
||||
|
||||
def show_organization_federal(self) -> bool:
|
||||
"""Show this step if the answer to the first question was "federal"."""
|
||||
user_choice = self.organization_type
|
||||
return user_choice == DomainApplication.OrganizationChoices.FEDERAL
|
||||
return user_choice == DomainRequest.OrganizationChoices.FEDERAL
|
||||
|
||||
def show_tribal_government(self) -> bool:
|
||||
"""Show this step if the answer to the first question was "tribal"."""
|
||||
user_choice = self.organization_type
|
||||
return user_choice == DomainApplication.OrganizationChoices.TRIBAL
|
||||
return user_choice == DomainRequest.OrganizationChoices.TRIBAL
|
||||
|
||||
def show_organization_election(self) -> bool:
|
||||
"""Show this step if the answer to the first question implies it.
|
||||
|
@ -890,9 +902,9 @@ class DomainApplication(TimeStampedModel):
|
|||
"""
|
||||
user_choice = self.organization_type
|
||||
excluded = [
|
||||
DomainApplication.OrganizationChoices.FEDERAL,
|
||||
DomainApplication.OrganizationChoices.INTERSTATE,
|
||||
DomainApplication.OrganizationChoices.SCHOOL_DISTRICT,
|
||||
DomainRequest.OrganizationChoices.FEDERAL,
|
||||
DomainRequest.OrganizationChoices.INTERSTATE,
|
||||
DomainRequest.OrganizationChoices.SCHOOL_DISTRICT,
|
||||
]
|
||||
return bool(user_choice and user_choice not in excluded)
|
||||
|
||||
|
@ -900,27 +912,27 @@ class DomainApplication(TimeStampedModel):
|
|||
"""Show this step if this is a special district or interstate."""
|
||||
user_choice = self.organization_type
|
||||
return user_choice in [
|
||||
DomainApplication.OrganizationChoices.SPECIAL_DISTRICT,
|
||||
DomainApplication.OrganizationChoices.INTERSTATE,
|
||||
DomainRequest.OrganizationChoices.SPECIAL_DISTRICT,
|
||||
DomainRequest.OrganizationChoices.INTERSTATE,
|
||||
]
|
||||
|
||||
def has_rationale(self) -> bool:
|
||||
"""Does this application have no_other_contacts_rationale?"""
|
||||
"""Does this domain request 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?"""
|
||||
"""Does this domain request have other contacts listed?"""
|
||||
return self.other_contacts.exists()
|
||||
|
||||
def is_federal(self) -> Union[bool, None]:
|
||||
"""Is this application for a federal agency?
|
||||
"""Is this domain request for a federal agency?
|
||||
|
||||
organization_type can be both null and blank,
|
||||
"""
|
||||
if not self.organization_type:
|
||||
# organization_type is either blank or None, can't answer
|
||||
return None
|
||||
if self.organization_type == DomainApplication.OrganizationChoices.FEDERAL:
|
||||
if self.organization_type == DomainRequest.OrganizationChoices.FEDERAL:
|
||||
return True
|
||||
return False
|
||||
|
|
@ -33,8 +33,8 @@ class UserGroup(Group):
|
|||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domainapplication",
|
||||
"permissions": ["change_domainapplication"],
|
||||
"model": "domainrequest",
|
||||
"permissions": ["change_domainrequest"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
|
|
|
@ -184,7 +184,7 @@ class DomainHelper:
|
|||
model_1_fields = set(field.name for field in model_1._meta.get_fields() if field.name != "id")
|
||||
model_2_fields = set(field.name for field in model_2._meta.get_fields() if field.name != "id")
|
||||
|
||||
# Get the fields that exist on both DomainApplication and DomainInformation
|
||||
# Get the fields that exist on both DomainRequest and DomainInformation
|
||||
common_fields = model_1_fields & model_2_fields
|
||||
|
||||
return common_fields
|
||||
|
|
|
@ -4,7 +4,7 @@ from .utility.time_stamped_model import TimeStampedModel
|
|||
|
||||
|
||||
class Website(TimeStampedModel):
|
||||
"""Keep domain names in their own table so that applications can refer to
|
||||
"""Keep domain names in their own table so that domain requests can refer to
|
||||
many of them."""
|
||||
|
||||
# domain names have strictly limited lengths, 255 characters is more than
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block title %}Domain request status | {{ domainapplication.requested_domain.name }} | {% endblock %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
<a href="{% url 'home' %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||
</svg>
|
||||
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
Back to manage your domains
|
||||
</p>
|
||||
</a>
|
||||
<h1>Domain request for {{ domainapplication.requested_domain.name }}</h1>
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-key-information"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
||||
id="summary-box-key-information"
|
||||
>
|
||||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</span>
|
||||
{% if domainapplication.status == 'approved' %} Approved
|
||||
{% elif domainapplication.status == 'in review' %} In review
|
||||
{% elif domainapplication.status == 'rejected' %} Rejected
|
||||
{% elif domainapplication.status == 'submitted' %} Submitted
|
||||
{% elif domainapplication.status == 'ineligible' %} Ineligible
|
||||
{% else %}ERROR Please contact technical support/dev
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p> <b class="review__step__name">Last updated:</b> {{domainapplication.updated_at|date:"F j, Y"}}<br>
|
||||
<b class="review__step__name">Request #:</b> {{domainapplication.id}}</p>
|
||||
<p>{% include "includes/domain_application.html" %}</p>
|
||||
<p><a href="{% url 'application-withdraw-confirmation' pk=domainapplication.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% with heading_level='h3' %}
|
||||
{% with org_type=domainapplication.get_organization_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if domainapplication.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=domainapplication.tribe_name heading_level=heading_level %}
|
||||
|
||||
{% if domainapplication.federally_recognized_tribe %}
|
||||
<p>Federally-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.state_recognized_tribe %}
|
||||
<p>State-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.get_federal_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Federal government branch' value=domainapplication.get_federal_type_display heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.is_election_board %}
|
||||
{% with value=domainapplication.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domainapplication address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.about_your_organization %}
|
||||
{% include "includes/summary_item.html" with title='About your organization' value=domainapplication.about_your_organization heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.authorizing_official %}
|
||||
{% include "includes/summary_item.html" with title='Authorizing official' value=domainapplication.authorizing_official contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.requested_domain %}
|
||||
{% include "includes/summary_item.html" with title='.gov domain' value=domainapplication.requested_domain heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.alternative_domains.all %}
|
||||
{% include "includes/summary_item.html" with title='Alternative domains' value=domainapplication.alternative_domains.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.purpose %}
|
||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=domainapplication.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.submitter %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=domainapplication.submitter contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Anything else?' value=domainapplication.anything_else|default:"No" heading_level=heading_level %}
|
||||
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -1,21 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Withdraw request for {{ domainapplication.requested_domain.name }} | {% endblock %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
|
||||
|
||||
<h1>Withdraw request for {{ domainapplication.requested_domain.name }}?</h1>
|
||||
|
||||
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
|
||||
|
||||
<p><a href="{% url 'application-withdrawn' domainapplication.id %}" class="usa-button withdraw">Withdraw request</a>
|
||||
<a href="{% url 'application-status' domainapplication.id %}">Cancel</a></p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load static field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -19,15 +19,16 @@
|
|||
|
||||
<h2>Next steps in this process</h2>
|
||||
|
||||
<p> We’ll review your request. This usually takes 20 business days. During
|
||||
this review we’ll verify that:</p>
|
||||
<p> We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.</p>
|
||||
|
||||
<p>During our review we’ll verify that:</p>
|
||||
<ul class="usa-list">
|
||||
<li>Your organization is eligible for a .gov domain.</li>
|
||||
<li>You work at the organization and/or can make requests on its behalf.</li>
|
||||
<li>Your requested domain meets our naming requirements.</li>
|
||||
</ul>
|
||||
|
||||
<p> We’ll email you if we have questions and when we complete our review. You can <a href="{% url 'home' %}">check the status</a>
|
||||
<p> We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can <a href="{% url 'home' %}">check the status</a>
|
||||
of your request at any time on the registrar homepage.</p>
|
||||
|
||||
<p> <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us if you need help during this process</a>.</p>
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -6,12 +6,12 @@
|
|||
<div class="grid-container">
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-3">
|
||||
{% include 'application_sidebar.html' %}
|
||||
{% include 'domain_request_sidebar.html' %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-9">
|
||||
<main id="main-content" class="grid-container register-form-step">
|
||||
{% if steps.prev %}
|
||||
<a href="{% namespaced_url 'application' steps.prev %}" class="breadcrumb__back">
|
||||
<a href="{% namespaced_url 'domain-request' steps.prev %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||
</svg><span class="margin-left-05">Previous step</span>
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load static field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load static url_helpers %}
|
||||
{% load custom_filters %}
|
||||
|
||||
|
@ -23,98 +23,98 @@
|
|||
<section class="summary-item margin-top-3">
|
||||
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.organization_type is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.get_organization_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.organization_type is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_organization_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.tribe_name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% if application.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if application.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% if domain_request.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if domain_request.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.get_federal_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.organization_name %}
|
||||
{% with title=form_titles|get_item:step value=application %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url address='true' %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.organization_name %}
|
||||
{% with title=form_titles|get_item:step value=domain_request %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url address='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='Incomplete' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.about_your_organization|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.AUTHORIZING_OFFICIAL %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.authorizing_official is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.authorizing_official %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.authorizing_official is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.authorizing_official %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.current_websites.all %}
|
||||
{% with title=form_titles|get_item:step value=application.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url list='true' %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.current_websites.all %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='None' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.requested_domain.name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
|
||||
{% if application.alternative_domains.all %}
|
||||
{% if domain_request.alternative_domains.all %}
|
||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for site in application.alternative_domains.all %}
|
||||
{% for site in domain_request.alternative_domains.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -122,51 +122,51 @@
|
|||
{% endif %}
|
||||
|
||||
{% if step == Step.PURPOSE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.purpose|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.submitter is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.submitter %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.submitter is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.submitter %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.other_contacts.all %}
|
||||
{% with title=form_titles|get_item:step value=application.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' list='true' %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.other_contacts.all %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value=application.no_other_contacts_rationale|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.anything_else|default:"No" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"No" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
</svg>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% namespaced_url 'application' this_step %}"
|
||||
<a href="{% namespaced_url 'domain-request' this_step %}"
|
||||
{% if this_step == steps.current %}
|
||||
class="usa-current"
|
||||
{% else %}
|
126
src/registrar/templates/domain_request_status.html
Normal file
126
src/registrar/templates/domain_request_status.html
Normal file
|
@ -0,0 +1,126 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block title %}Domain request status | {{ DomainRequest.requested_domain.name }} | {% endblock %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
<a href="{% url 'home' %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||
</svg>
|
||||
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
Back to manage your domains
|
||||
</p>
|
||||
</a>
|
||||
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-key-information"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
||||
id="summary-box-key-information"
|
||||
>
|
||||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</span>
|
||||
{% if DomainRequest.status == 'approved' %} Approved
|
||||
{% elif DomainRequest.status == 'in review' %} In review
|
||||
{% elif DomainRequest.status == 'rejected' %} Rejected
|
||||
{% elif DomainRequest.status == 'submitted' %} Submitted
|
||||
{% elif DomainRequest.status == 'ineligible' %} Ineligible
|
||||
{% else %}ERROR Please contact technical support/dev
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p> <b class="review__step__name">Last updated:</b> {{DomainRequest.updated_at|date:"F j, Y"}}<br>
|
||||
<b class="review__step__name">Request #:</b> {{DomainRequest.id}}</p>
|
||||
<p>{% include "includes/domain_request.html" %}</p>
|
||||
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% with heading_level='h3' %}
|
||||
{% with org_type=DomainRequest.get_organization_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if DomainRequest.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
||||
|
||||
{% if DomainRequest.federally_recognized_tribe %}
|
||||
<p>Federally-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.state_recognized_tribe %}
|
||||
<p>State-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.get_federal_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.is_election_board %}
|
||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.about_your_organization %}
|
||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.authorizing_official %}
|
||||
{% include "includes/summary_item.html" with title='Authorizing official' value=DomainRequest.authorizing_official contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.requested_domain %}
|
||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.alternative_domains.all %}
|
||||
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.purpose %}
|
||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.submitter %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.submitter contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Anything else?' value=DomainRequest.anything_else|default:"No" heading_level=heading_level %}
|
||||
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -0,0 +1,21 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Withdraw request for {{ DomainRequest.requested_domain.name }} | {% endblock %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
|
||||
|
||||
<h1>Withdraw request for {{ DomainRequest.requested_domain.name }}?</h1>
|
||||
|
||||
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
|
||||
|
||||
<p><a href="{% url 'domain-request-withdrawn' DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
|
||||
<a href="{% url 'domain-request-status' DomainRequest.id %}">Cancel</a></p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% extends 'domain_request_form.html' %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
|
@ -1,10 +1,10 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ application.submitter.first_name }}.
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
Your .gov domain request has been withdrawn and will not be reviewed by our team.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Withdrawn
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
|
|
@ -1 +1 @@
|
|||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
SUMMARY OF YOUR DOMAIN REQUEST
|
||||
|
||||
Type of organization:
|
||||
{{ application.get_organization_type_display }}
|
||||
{% if application.show_organization_federal %}
|
||||
Federal government branch:
|
||||
{{ application.get_federal_type_display }}
|
||||
{% elif application.show_tribal_government %}
|
||||
Tribal government:
|
||||
{{ application.tribe_name|default:"Incomplete" }}{% if application.federally_recognized_tribe %}
|
||||
Federally-recognized tribe
|
||||
{% endif %}{% if application.state_recognized_tribe %}
|
||||
State-recognized tribe
|
||||
{% endif %}{% endif %}{% if application.show_organization_election %}
|
||||
Election office:
|
||||
{{ application.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
Organization name and mailing address:
|
||||
{% spaceless %}{{ application.federal_agency }}
|
||||
{{ application.organization_name }}
|
||||
{{ application.address_line1 }}{% if application.address_line2 %}
|
||||
{{ application.address_line2 }}{% endif %}
|
||||
{{ application.city }}, {{ application.state_territory }}
|
||||
{{ application.zipcode }}{% if application.urbanization %}
|
||||
{{ application.urbanization }}{% endif %}{% endspaceless %}
|
||||
{% if application.about_your_organization %}{# if block makes one newline if it's false #}
|
||||
About your organization:
|
||||
{% spaceless %}{{ application.about_your_organization }}{% endspaceless %}
|
||||
{% endif %}
|
||||
Authorizing official:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
|
||||
{% if application.current_websites.exists %}{# if block makes a newline #}
|
||||
Current websites: {% for site in application.current_websites.all %}
|
||||
{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
{{ application.requested_domain.name }}
|
||||
{% if application.alternative_domains.all %}
|
||||
Alternative domains:
|
||||
{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
Purpose of your domain:
|
||||
{{ application.purpose }}
|
||||
|
||||
Your contact information:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %}
|
||||
|
||||
Other employees from your organization:{% for other in application.other_contacts.all %}
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
|
||||
{% empty %}
|
||||
{{ application.no_other_contacts_rationale }}
|
||||
{% endfor %}{% if application.anything_else %}
|
||||
Anything else?
|
||||
{{ application.anything_else }}
|
||||
{% endif %}
|
|
@ -0,0 +1,55 @@
|
|||
SUMMARY OF YOUR DOMAIN REQUEST
|
||||
|
||||
Type of organization:
|
||||
{{ domain_request.get_organization_type_display }}
|
||||
{% if domain_request.show_organization_federal %}
|
||||
Federal government branch:
|
||||
{{ domain_request.get_federal_type_display }}
|
||||
{% elif domain_request.show_tribal_government %}
|
||||
Tribal government:
|
||||
{{ domain_request.tribe_name|default:"Incomplete" }}{% if domain_request.federally_recognized_tribe %}
|
||||
Federally-recognized tribe
|
||||
{% endif %}{% if domain_request.state_recognized_tribe %}
|
||||
State-recognized tribe
|
||||
{% endif %}{% endif %}{% if domain_request.show_organization_election %}
|
||||
Election office:
|
||||
{{ domain_request.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
Organization name and mailing address:
|
||||
{% spaceless %}{{ domain_request.federal_agency }}
|
||||
{{ domain_request.organization_name }}
|
||||
{{ domain_request.address_line1 }}{% if domain_request.address_line2 %}
|
||||
{{ domain_request.address_line2 }}{% endif %}
|
||||
{{ domain_request.city }}, {{ domain_request.state_territory }}
|
||||
{{ domain_request.zipcode }}{% if domain_request.urbanization %}
|
||||
{{ domain_request.urbanization }}{% endif %}{% endspaceless %}
|
||||
{% if domain_request.about_your_organization %}{# if block makes one newline if it's false #}
|
||||
About your organization:
|
||||
{% spaceless %}{{ domain_request.about_your_organization }}{% endspaceless %}
|
||||
{% endif %}
|
||||
Authorizing official:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=domain_request.authorizing_official %}{% endspaceless %}
|
||||
{% if domain_request.current_websites.exists %}{# if block makes a newline #}
|
||||
Current websites: {% for site in domain_request.current_websites.all %}
|
||||
{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
{{ domain_request.requested_domain.name }}
|
||||
{% if domain_request.alternative_domains.all %}
|
||||
Alternative domains:
|
||||
{% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
Purpose of your domain:
|
||||
{{ domain_request.purpose }}
|
||||
|
||||
Your contact information:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=domain_request.submitter %}{% endspaceless %}
|
||||
|
||||
Other employees from your organization:{% for other in domain_request.other_contacts.all %}
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
|
||||
{% empty %}
|
||||
{{ domain_request.no_other_contacts_rationale }}
|
||||
{% endfor %}{% if domain_request.anything_else %}
|
||||
Anything else?
|
||||
{{ domain_request.anything_else }}
|
||||
{% endif %}
|
1
src/registrar/templates/emails/metadata_body.txt
Normal file
1
src/registrar/templates/emails/metadata_body.txt
Normal file
|
@ -0,0 +1 @@
|
|||
An export of all .gov metadata.
|
2
src/registrar/templates/emails/metadata_subject.txt
Normal file
2
src/registrar/templates/emails/metadata_subject.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Domain metadata - {{current_date_str}}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ application.submitter.first_name }}.
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
Congratulations! Your .gov domain request has been approved.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Approved
|
||||
|
||||
You can manage your approved domain on the .gov registrar <https://manage.get.gov>.
|
||||
|
|
|
@ -1 +1 @@
|
|||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ application.submitter.first_name }}.
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
Your .gov domain request has been rejected.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Rejected
|
||||
|
||||
----------------------------------------------------------------
|
||||
{% if application.rejection_reason != 'other' %}
|
||||
REJECTION REASON{% endif %}{% if application.rejection_reason == 'purpose_not_met' %}
|
||||
{% if domain_request.rejection_reason != 'other' %}
|
||||
REJECTION REASON{% endif %}{% if domain_request.rejection_reason == 'purpose_not_met' %}
|
||||
Your domain request was rejected because the purpose you provided did not meet our
|
||||
requirements. You didn’t provide enough information about how you intend to use the
|
||||
domain.
|
||||
|
@ -18,16 +18,16 @@ Learn more about:
|
|||
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
||||
- What you can and can’t do with .gov domains <https://get.gov/domains/requirements/>
|
||||
|
||||
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor_not_eligible' %}
|
||||
If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'requestor_not_eligible' %}
|
||||
Your domain request was rejected because we don’t believe you’re eligible to request a
|
||||
.gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be
|
||||
.gov domain on behalf of {{ domain_request.organization_name }}. You must be a government employee, or be
|
||||
working on behalf of a government organization, to request a .gov domain.
|
||||
|
||||
|
||||
DEMONSTRATE ELIGIBILITY
|
||||
If you can provide more information that demonstrates your eligibility, or you want to
|
||||
discuss further, reply to this email.{% elif application.rejection_reason == 'org_has_domain' %}
|
||||
Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our
|
||||
discuss further, reply to this email.{% elif domain_request.rejection_reason == 'org_has_domain' %}
|
||||
Your domain request was rejected because {{ domain_request.organization_name }} has a .gov domain. Our
|
||||
practice is to approve one domain per online service per government organization. We
|
||||
evaluate additional requests on a case-by-case basis. You did not provide sufficient
|
||||
justification for an additional domain.
|
||||
|
@ -35,10 +35,10 @@ justification for an additional domain.
|
|||
Read more about our practice of approving one domain per online service
|
||||
<https://get.gov/domains/before/#one-domain-per-service>.
|
||||
|
||||
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_not_verified' %}
|
||||
If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'contacts_not_verified' %}
|
||||
Your domain request was rejected because we could not verify the organizational
|
||||
contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'org_not_eligible' %}
|
||||
Your domain request was rejected because we determined that {{ application.organization_name }} is not
|
||||
contacts you provided. If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'org_not_eligible' %}
|
||||
Your domain request was rejected because we determined that {{ domain_request.organization_name }} is not
|
||||
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
|
||||
government organizations.
|
||||
|
||||
|
@ -48,7 +48,7 @@ If you can provide documentation that demonstrates your eligibility, reply to th
|
|||
This can include links to (or copies of) your authorizing legislation, your founding
|
||||
charter or bylaws, or other similar documentation. Without this, we can’t approve a
|
||||
.gov domain for your organization. Learn more about eligibility for .gov domains
|
||||
<https://get.gov/domains/eligibility/>.{% elif application.rejection_reason == 'naming_not_met' %}
|
||||
<https://get.gov/domains/eligibility/>.{% elif domain_request.rejection_reason == 'naming_not_met' %}
|
||||
Your domain request was rejected because it does not meet our naming requirements.
|
||||
Domains should uniquely identify a government organization and be clear to the
|
||||
general public. Learn more about naming requirements for your type of organization
|
||||
|
@ -57,7 +57,7 @@ general public. Learn more about naming requirements for your type of organizati
|
|||
|
||||
YOU CAN SUBMIT A NEW REQUEST
|
||||
We encourage you to request a domain that meets our requirements. If you have
|
||||
questions or want to discuss potential domain names, reply to this email.{% elif application.rejection_reason == 'other' %}
|
||||
questions or want to discuss potential domain names, reply to this email.{% elif domain_request.rejection_reason == 'other' %}
|
||||
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.
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ application.submitter.first_name }}.
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
We received your .gov domain request.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Submitted
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
NEXT STEPS
|
||||
We’ll review your request. This usually takes 20 business days. During this review we’ll verify that:
|
||||
We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.
|
||||
|
||||
During our 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>
|
||||
We’ll email you if we have questions. We’ll also email you as soon as 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?
|
||||
|
@ -29,7 +31,7 @@ THANK YOU
|
|||
|
||||
----------------------------------------------------------------
|
||||
|
||||
{% include 'emails/includes/application_summary.txt' %}
|
||||
{% include 'emails/includes/domain_request_summary.txt' %}
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
|
|
|
@ -1 +1 @@
|
|||
Update on your .gov request: {{ application.requested_domain.name }}
|
||||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
|
||||
<p class="margin-top-4">
|
||||
<a href="{% url 'application:' %}" class="usa-button"
|
||||
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||
>
|
||||
Start a new domain request
|
||||
</a>
|
||||
|
@ -103,7 +103,7 @@
|
|||
|
||||
<section class="section--outlined">
|
||||
<h2>Domain requests</h2>
|
||||
{% if domain_applications %}
|
||||
{% if domain_requests %}
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||
<caption class="sr-only">Your domain requests</caption>
|
||||
<thead>
|
||||
|
@ -112,61 +112,61 @@
|
|||
<th data-sortable scope="col" role="columnheader">Date submitted</th>
|
||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||
{% if has_deletable_applications %}
|
||||
{% if has_deletable_domain_requests %}
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Delete Action</span></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for application in domain_applications %}
|
||||
{% for domain_request in domain_requests %}
|
||||
<tr>
|
||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||
{% if application.requested_domain is None %}
|
||||
{% if domain_request.requested_domain is None %}
|
||||
New domain request
|
||||
{# Add a breakpoint #}
|
||||
<div aria-hidden="true"></div>
|
||||
<span class="text-base font-body-xs">({{ application.created_at }} UTC)</span>
|
||||
<span class="text-base font-body-xs">({{ domain_request.created_at }} UTC)</span>
|
||||
{% else %}
|
||||
{{ application.requested_domain.name }}
|
||||
{{ domain_request.requested_domain.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
|
||||
{% if application.submission_date %}
|
||||
{{ application.submission_date|date }}
|
||||
<td data-sort-value="{{ domain_request.submission_date|date:"U" }}" data-label="Date submitted">
|
||||
{% if domain_request.submission_date %}
|
||||
{{ domain_request.submission_date|date }}
|
||||
{% else %}
|
||||
<span class="text-base">Not submitted</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-label="Status">{{ application.get_status_display }}</td>
|
||||
<td data-label="Status">{{ domain_request.get_status_display }}</td>
|
||||
<td>
|
||||
{% with prefix="New domain request ("%}
|
||||
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with date=domain_request.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||
{% if application.status == application.ApplicationStatus.STARTED or application.status == application.ApplicationStatus.ACTION_NEEDED or application.status == application.ApplicationStatus.WITHDRAWN %}
|
||||
<a href="{% url 'edit-application' application.pk %}">
|
||||
{% if domain_request.status == domain_request.DomainRequestStatus.STARTED or domain_request.status == domain_request.DomainRequestStatus.ACTION_NEEDED or domain_request.status == domain_request.DomainRequestStatus.WITHDRAWN %}
|
||||
<a href="{% url 'edit-domain-request' domain_request.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||
</svg>
|
||||
{% if application.requested_domain is not None%}
|
||||
Edit <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||
{% if domain_request.requested_domain is not None%}
|
||||
Edit <span class="usa-sr-only">{{ domain_request.requested_domain.name }}</span>
|
||||
{% else %}
|
||||
Edit <span class="usa-sr-only">{{ name_default }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'application-status' application.pk %}">
|
||||
<a href="{% url 'domain-request-status' domain_request.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
||||
</svg>
|
||||
Manage <span class="usa-sr-only">{{ application.requested_domain.name|default:name_default }}</span>
|
||||
Manage <span class="usa-sr-only">{{ domain_request.requested_domain.name|default:name_default }}</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
</td>
|
||||
{% if has_deletable_applications %}
|
||||
{% if has_deletable_domain_requests %}
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "withdrawn" %}
|
||||
{% if domain_request.status == "started" or domain_request.status == "withdrawn" %}
|
||||
<a
|
||||
role="button"
|
||||
id="button-toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
|
@ -179,10 +179,10 @@
|
|||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>
|
||||
{% with prefix="New domain request ("%}
|
||||
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with date=domain_request.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||
{% if application.requested_domain is not None %}
|
||||
Delete <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||
{% if domain_request.requested_domain is not None %}
|
||||
Delete <span class="usa-sr-only">{{ domain_request.requested_domain.name }}</span>
|
||||
{% else %}
|
||||
Delete <span class="usa-sr-only">{{ name_default }}</span>
|
||||
{% endif %}
|
||||
|
@ -198,11 +198,11 @@
|
|||
aria-describedby="Domain will be removed"
|
||||
data-force-action
|
||||
>
|
||||
<form method="POST" action="{% url "application-delete" pk=application.id %}">
|
||||
{% if application.requested_domain is None %}
|
||||
{% if application.created_at %}
|
||||
<form method="POST" action="{% url "domain-request-delete" pk=domain_request.id %}">
|
||||
{% if domain_request.requested_domain is None %}
|
||||
{% if domain_request.created_at %}
|
||||
{% with prefix="(created " %}
|
||||
{% with formatted_date=application.created_at|date:"DATETIME_FORMAT" %}
|
||||
{% with formatted_date=domain_request.created_at|date:"DATETIME_FORMAT" %}
|
||||
{% with modal_content=prefix|add:formatted_date|add:" UTC)" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this domain request?" modal_description="This will remove the domain request "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endwith %}
|
||||
|
@ -212,7 +212,7 @@
|
|||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% with modal_heading_value=application.requested_domain.name|add:"?" %}
|
||||
{% with modal_heading_value=domain_request.requested_domain.name|add:"?" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value=modal_heading_value modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -231,7 +231,7 @@
|
|||
></div>
|
||||
{% else %}
|
||||
<p>You haven't requested any domains.</p>
|
||||
<!-- <p><a href="{% url 'application:' %}" class="usa-button">Start a new domain request</a></p> -->
|
||||
<!-- <p><a href="{% url 'domain-request:' %}" class="usa-button">Start a new domain request</a></p> -->
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from django import template
|
||||
import re
|
||||
from registrar.models.domain_application import DomainApplication
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
|
||||
register = template.Library()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -55,7 +55,7 @@ def contains_checkbox(html_list):
|
|||
|
||||
@register.filter
|
||||
def get_organization_long_name(organization_type):
|
||||
organization_choices_dict = dict(DomainApplication.OrganizationChoicesVerbose.choices)
|
||||
organization_choices_dict = dict(DomainRequest.OrganizationChoicesVerbose.choices)
|
||||
long_form_type = organization_choices_dict[organization_type]
|
||||
if long_form_type is None:
|
||||
logger.error("Organization type error, triggered by a template's custom filter")
|
||||
|
|
|
@ -19,7 +19,7 @@ from registrar.models import (
|
|||
Contact,
|
||||
DraftDomain,
|
||||
Website,
|
||||
DomainApplication,
|
||||
DomainRequest,
|
||||
DomainInvitation,
|
||||
User,
|
||||
UserGroup,
|
||||
|
@ -223,7 +223,7 @@ class AuditedAdminMockData:
|
|||
|
||||
# Constants for different domain object types
|
||||
INFORMATION = "information"
|
||||
APPLICATION = "application"
|
||||
DOMAIN_REQUEST = "domain_request"
|
||||
INVITATION = "invitation"
|
||||
|
||||
def dummy_user(self, item_name, short_hand):
|
||||
|
@ -367,24 +367,24 @@ class AuditedAdminMockData:
|
|||
self,
|
||||
domain_type,
|
||||
item_name,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
org_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
):
|
||||
"""
|
||||
Returns a prebuilt kwarg dictionary for DomainApplication,
|
||||
Returns a prebuilt kwarg dictionary for DomainRequest,
|
||||
DomainInformation, or DomainInvitation.
|
||||
Args:
|
||||
domain_type (str): is either 'application', 'information',
|
||||
domain_type (str): is either 'domain_request', 'information',
|
||||
or 'invitation'.
|
||||
|
||||
item_name (str): A shared str value appended to first_name, last_name,
|
||||
organization_name, address_line1, address_line2,
|
||||
title, email, and username.
|
||||
|
||||
status (str - optional): Defines the status for DomainApplication,
|
||||
e.g. DomainApplication.ApplicationStatus.STARTED
|
||||
status (str - optional): Defines the status for DomainRequest,
|
||||
e.g. DomainRequest.DomainRequestStatus.STARTED
|
||||
|
||||
org_type (str - optional): Sets a domains org_type
|
||||
|
||||
|
@ -393,13 +393,13 @@ class AuditedAdminMockData:
|
|||
purpose (str - optional): Sets a domains purpose
|
||||
Returns:
|
||||
dict: Returns a dictionary structurally consistent with the expected input
|
||||
of either DomainApplication, DomainInvitation, or DomainInformation
|
||||
of either DomainRequest, DomainInvitation, or DomainInformation
|
||||
based on the 'domain_type' field.
|
||||
""" # noqa
|
||||
common_args = self.get_common_domain_arg_dictionary(item_name, org_type, federal_type, purpose)
|
||||
full_arg_dict = None
|
||||
match domain_type:
|
||||
case self.APPLICATION:
|
||||
case self.DOMAIN_REQUEST:
|
||||
full_arg_dict = dict(
|
||||
**common_args,
|
||||
requested_domain=self.dummy_draft_domain(item_name),
|
||||
|
@ -407,11 +407,11 @@ class AuditedAdminMockData:
|
|||
status=status,
|
||||
)
|
||||
case self.INFORMATION:
|
||||
domain_app = self.create_full_dummy_domain_application(item_name)
|
||||
domain_req = self.create_full_dummy_domain_request(item_name)
|
||||
full_arg_dict = dict(
|
||||
**common_args,
|
||||
domain=self.dummy_domain(item_name, True),
|
||||
domain_application=domain_app,
|
||||
domain_request=domain_req,
|
||||
)
|
||||
case self.INVITATION:
|
||||
full_arg_dict = dict(
|
||||
|
@ -421,24 +421,24 @@ class AuditedAdminMockData:
|
|||
)
|
||||
return full_arg_dict
|
||||
|
||||
def create_full_dummy_domain_application(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
|
||||
"""Creates a dummy domain application object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.APPLICATION, item_name, status)
|
||||
application = DomainApplication.objects.get_or_create(**domain_application_kwargs)[0]
|
||||
return application
|
||||
def create_full_dummy_domain_request(self, item_name, status=DomainRequest.DomainRequestStatus.STARTED):
|
||||
"""Creates a dummy domain request object"""
|
||||
domain_request_kwargs = self.dummy_kwarg_boilerplate(self.DOMAIN_REQUEST, item_name, status)
|
||||
domain_request = DomainRequest.objects.get_or_create(**domain_request_kwargs)[0]
|
||||
return domain_request
|
||||
|
||||
def create_full_dummy_domain_information(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
|
||||
def create_full_dummy_domain_information(self, item_name, status=DomainRequest.DomainRequestStatus.STARTED):
|
||||
"""Creates a dummy domain information object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INFORMATION, item_name, status)
|
||||
application = DomainInformation.objects.get_or_create(**domain_application_kwargs)[0]
|
||||
return application
|
||||
domain_request_kwargs = self.dummy_kwarg_boilerplate(self.INFORMATION, item_name, status)
|
||||
domain_request = DomainInformation.objects.get_or_create(**domain_request_kwargs)[0]
|
||||
return domain_request
|
||||
|
||||
def create_full_dummy_domain_invitation(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
|
||||
def create_full_dummy_domain_invitation(self, item_name, status=DomainRequest.DomainRequestStatus.STARTED):
|
||||
"""Creates a dummy domain invitation object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INVITATION, item_name, status)
|
||||
application = DomainInvitation.objects.get_or_create(**domain_application_kwargs)[0]
|
||||
domain_request_kwargs = self.dummy_kwarg_boilerplate(self.INVITATION, item_name, status)
|
||||
domain_request = DomainInvitation.objects.get_or_create(**domain_request_kwargs)[0]
|
||||
|
||||
return application
|
||||
return domain_request
|
||||
|
||||
def create_full_dummy_domain_object(
|
||||
self,
|
||||
|
@ -447,31 +447,31 @@ class AuditedAdminMockData:
|
|||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
):
|
||||
"""A helper to create a dummy domain application object"""
|
||||
application = None
|
||||
"""A helper to create a dummy domain request object"""
|
||||
domain_request = None
|
||||
match domain_type:
|
||||
case self.APPLICATION:
|
||||
application = self.create_full_dummy_domain_application(item_name, status)
|
||||
case self.DOMAIN_REQUEST:
|
||||
domain_request = self.create_full_dummy_domain_request(item_name, status)
|
||||
case self.INVITATION:
|
||||
application = self.create_full_dummy_domain_invitation(item_name, status)
|
||||
domain_request = self.create_full_dummy_domain_invitation(item_name, status)
|
||||
case self.INFORMATION:
|
||||
application = self.create_full_dummy_domain_information(item_name, status)
|
||||
domain_request = self.create_full_dummy_domain_information(item_name, status)
|
||||
case _:
|
||||
raise ValueError("Invalid domain_type, must conform to given constants")
|
||||
|
||||
if has_other_contacts and domain_type != self.INVITATION:
|
||||
other = self.dummy_contact(item_name, "other")
|
||||
application.other_contacts.add(other)
|
||||
if has_current_website and domain_type == self.APPLICATION:
|
||||
domain_request.other_contacts.add(other)
|
||||
if has_current_website and domain_type == self.DOMAIN_REQUEST:
|
||||
current = self.dummy_current(item_name)
|
||||
application.current_websites.add(current)
|
||||
if has_alternative_gov_domain and domain_type == self.APPLICATION:
|
||||
domain_request.current_websites.add(current)
|
||||
if has_alternative_gov_domain and domain_type == self.DOMAIN_REQUEST:
|
||||
alt = self.dummy_alt(item_name)
|
||||
application.alternative_domains.add(alt)
|
||||
domain_request.alternative_domains.add(alt)
|
||||
|
||||
return application
|
||||
return domain_request
|
||||
|
||||
|
||||
class MockDb(TestCase):
|
||||
|
@ -699,18 +699,19 @@ def create_ready_domain():
|
|||
return domain
|
||||
|
||||
|
||||
def completed_application(
|
||||
def completed_domain_request(
|
||||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
has_about_your_organization=True,
|
||||
has_anything_else=True,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
user=False,
|
||||
submitter=False,
|
||||
name="city.gov",
|
||||
investigator=None,
|
||||
):
|
||||
"""A completed domain application."""
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
user = get_user_model().objects.create(username="username" + str(uuid.uuid4())[:8])
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
|
@ -738,7 +739,14 @@ def completed_application(
|
|||
email="testy2@town.com",
|
||||
phone="(555) 555 5557",
|
||||
)
|
||||
domain_application_kwargs = dict(
|
||||
if not investigator:
|
||||
investigator, _ = User.objects.get_or_create(
|
||||
username="incrediblyfakeinvestigator",
|
||||
first_name="Joe",
|
||||
last_name="Bob",
|
||||
is_staff=True,
|
||||
)
|
||||
domain_request_kwargs = dict(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
|
@ -753,45 +761,53 @@ def completed_application(
|
|||
submitter=submitter,
|
||||
creator=user,
|
||||
status=status,
|
||||
investigator=investigator,
|
||||
)
|
||||
if has_about_your_organization:
|
||||
domain_application_kwargs["about_your_organization"] = "e-Government"
|
||||
domain_request_kwargs["about_your_organization"] = "e-Government"
|
||||
if has_anything_else:
|
||||
domain_application_kwargs["anything_else"] = "There is more"
|
||||
domain_request_kwargs["anything_else"] = "There is more"
|
||||
|
||||
application, _ = DomainApplication.objects.get_or_create(**domain_application_kwargs)
|
||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||
|
||||
if has_other_contacts:
|
||||
application.other_contacts.add(other)
|
||||
domain_request.other_contacts.add(other)
|
||||
if has_current_website:
|
||||
application.current_websites.add(current)
|
||||
domain_request.current_websites.add(current)
|
||||
if has_alternative_gov_domain:
|
||||
application.alternative_domains.add(alt)
|
||||
domain_request.alternative_domains.add(alt)
|
||||
|
||||
return application
|
||||
return domain_request
|
||||
|
||||
|
||||
def set_domain_request_investigators(domain_request_list: list[DomainRequest], investigator_user: User):
|
||||
"""Helper method that sets the investigator field of all provided domain requests"""
|
||||
for request in domain_request_list:
|
||||
request.investigator = investigator_user
|
||||
request.save()
|
||||
|
||||
|
||||
def multiple_unalphabetical_domain_objects(
|
||||
domain_type=AuditedAdminMockData.APPLICATION,
|
||||
domain_type=AuditedAdminMockData.DOMAIN_REQUEST,
|
||||
):
|
||||
"""Returns a list of generic domain objects for testing purposes"""
|
||||
applications = []
|
||||
domain_requests = []
|
||||
list_of_letters = list(ascii_uppercase)
|
||||
random.shuffle(list_of_letters)
|
||||
|
||||
mock = AuditedAdminMockData()
|
||||
for object_name in list_of_letters:
|
||||
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||
applications.append(application)
|
||||
return applications
|
||||
domain_request = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||
domain_requests.append(domain_request)
|
||||
return domain_requests
|
||||
|
||||
|
||||
def generic_domain_object(domain_type, object_name):
|
||||
"""Returns a generic domain object of
|
||||
domain_type 'application', 'information', or 'invitation'"""
|
||||
domain_type 'domain_request', 'information', or 'invitation'"""
|
||||
mock = AuditedAdminMockData()
|
||||
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||
return application
|
||||
domain_request = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||
return domain_request
|
||||
|
||||
|
||||
class MockEppLib(TestCase):
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,9 +3,10 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
from django.test import TestCase
|
||||
from .common import completed_application, less_console_noise
|
||||
|
||||
from .common import completed_domain_request, less_console_noise
|
||||
|
||||
from datetime import datetime
|
||||
from registrar.utility import email
|
||||
import boto3_mocking # type: ignore
|
||||
|
||||
|
||||
|
@ -17,11 +18,11 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation(self):
|
||||
"""Submission confirmation email works."""
|
||||
application = completed_application()
|
||||
domain_request = completed_domain_request()
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
|
||||
# check that an email was sent
|
||||
self.assertTrue(self.mock_client.send_email.called)
|
||||
|
@ -55,10 +56,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_current_website_spacing(self):
|
||||
"""Test line spacing without current_website."""
|
||||
application = completed_application(has_current_website=False)
|
||||
domain_request = completed_domain_request(has_current_website=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("Current websites:", body)
|
||||
|
@ -68,10 +69,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_current_website_spacing(self):
|
||||
"""Test line spacing with current_website."""
|
||||
application = completed_application(has_current_website=True)
|
||||
domain_request = completed_domain_request(has_current_website=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("Current websites:", body)
|
||||
|
@ -82,10 +83,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_other_contacts_spacing(self):
|
||||
"""Test line spacing with other contacts."""
|
||||
application = completed_application(has_other_contacts=True)
|
||||
domain_request = completed_domain_request(has_other_contacts=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("Other employees from your organization:", body)
|
||||
|
@ -96,10 +97,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_other_contacts_spacing(self):
|
||||
"""Test line spacing without other contacts."""
|
||||
application = completed_application(has_other_contacts=False)
|
||||
domain_request = completed_domain_request(has_other_contacts=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
# spacing should be right between adjacent elements
|
||||
|
@ -109,10 +110,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_alternative_govdomain_spacing(self):
|
||||
"""Test line spacing with alternative .gov domain."""
|
||||
application = completed_application(has_alternative_gov_domain=True)
|
||||
domain_request = completed_domain_request(has_alternative_gov_domain=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("city1.gov", body)
|
||||
|
@ -122,10 +123,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_alternative_govdomain_spacing(self):
|
||||
"""Test line spacing without alternative .gov domain."""
|
||||
application = completed_application(has_alternative_gov_domain=False)
|
||||
domain_request = completed_domain_request(has_alternative_gov_domain=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("city1.gov", body)
|
||||
|
@ -135,10 +136,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_about_your_organization_spacing(self):
|
||||
"""Test line spacing with about your organization."""
|
||||
application = completed_application(has_about_your_organization=True)
|
||||
domain_request = completed_domain_request(has_about_your_organization=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("About your organization:", body)
|
||||
|
@ -148,10 +149,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_about_your_organization_spacing(self):
|
||||
"""Test line spacing without about your organization."""
|
||||
application = completed_application(has_about_your_organization=False)
|
||||
domain_request = completed_domain_request(has_about_your_organization=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("About your organization:", body)
|
||||
|
@ -161,10 +162,10 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_anything_else_spacing(self):
|
||||
"""Test line spacing with anything else."""
|
||||
application = completed_application(has_anything_else=True)
|
||||
domain_request = completed_domain_request(has_anything_else=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
# spacing should be right between adjacent elements
|
||||
|
@ -173,12 +174,41 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_anything_else_spacing(self):
|
||||
"""Test line spacing without anything else."""
|
||||
application = completed_application(has_anything_else=False)
|
||||
domain_request = completed_domain_request(has_anything_else=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("Anything else", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5557\n\n----")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_send_email_with_attachment(self):
|
||||
with boto3_mocking.clients.handler_for("ses", self.mock_client_class):
|
||||
sender_email = "sender@example.com"
|
||||
recipient_email = "recipient@example.com"
|
||||
subject = "Test Subject"
|
||||
body = "Test Body"
|
||||
attachment_file = b"Attachment file content"
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.zip"
|
||||
|
||||
email.send_email_with_attachment(
|
||||
sender_email, recipient_email, subject, body, attachment_file, self.mock_client
|
||||
)
|
||||
# Assert that the `send_raw_email` method of the mocked SES client was called with the expected params
|
||||
self.mock_client.send_raw_email.assert_called_once()
|
||||
|
||||
# Get the args passed to the `send_raw_email` method
|
||||
call_args = self.mock_client.send_raw_email.call_args[1]
|
||||
|
||||
# Assert that the attachment filename is correct
|
||||
self.assertEqual(call_args["RawMessage"]["Data"].count(f'filename="{current_filename}"'), 1)
|
||||
|
||||
# Assert that the attachment content is encrypted
|
||||
self.assertIn("Content-Type: application/octet-stream", call_args["RawMessage"]["Data"])
|
||||
self.assertIn("Content-Transfer-Encoding: base64", call_args["RawMessage"]["Data"])
|
||||
self.assertIn("Content-Disposition: attachment;", call_args["RawMessage"]["Data"])
|
||||
self.assertNotIn("Attachment file content", call_args["RawMessage"]["Data"])
|
||||
|
|
|
@ -4,7 +4,7 @@ import json
|
|||
from django.test import TestCase, RequestFactory
|
||||
from api.views import available
|
||||
|
||||
from registrar.forms.application_wizard import (
|
||||
from registrar.forms.domain_request_wizard import (
|
||||
AlternativeDomainForm,
|
||||
CurrentSitesForm,
|
||||
DotGovDomainForm,
|
||||
|
|
|
@ -34,10 +34,10 @@ class TestGroups(TestCase):
|
|||
"view_logentry",
|
||||
"change_contact",
|
||||
"view_domain",
|
||||
"change_domainapplication",
|
||||
"change_domaininformation",
|
||||
"add_domaininvitation",
|
||||
"view_domaininvitation",
|
||||
"change_domainrequest",
|
||||
"change_draftdomain",
|
||||
"analyst_access_permission",
|
||||
"change_user",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,7 +12,7 @@ from django.utils.timezone import make_aware
|
|||
from registrar.models import Domain, Host, HostIP
|
||||
|
||||
from unittest import skip
|
||||
from registrar.models.domain_application import DomainApplication
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.draft_domain import DraftDomain
|
||||
from registrar.models.public_contact import PublicContact
|
||||
|
@ -311,27 +311,30 @@ class TestDomainCache(MockEppLib):
|
|||
|
||||
|
||||
class TestDomainCreation(MockEppLib):
|
||||
"""Rule: An approved domain application must result in a domain"""
|
||||
"""Rule: An approved domain request must result in a domain"""
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_approved_application_creates_domain_locally(self):
|
||||
def test_approved_domain_request_creates_domain_locally(self):
|
||||
"""
|
||||
Scenario: Analyst approves a domain application
|
||||
When the DomainApplication transitions to approved
|
||||
Scenario: Analyst approves a domain request
|
||||
When the DomainRequest transitions to approved
|
||||
Then a Domain exists in the database with the same `name`
|
||||
But a domain object does not exist in the registry
|
||||
"""
|
||||
with less_console_noise():
|
||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
||||
investigator, _ = User.objects.get_or_create(username="frenchtoast", is_staff=True)
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=user, requested_domain=draft_domain, investigator=investigator
|
||||
)
|
||||
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# skip using the submit method
|
||||
application.status = DomainApplication.ApplicationStatus.SUBMITTED
|
||||
domain_request.status = DomainRequest.DomainRequestStatus.SUBMITTED
|
||||
# transition to approve state
|
||||
application.approve()
|
||||
domain_request.approve()
|
||||
# should have information present for this domain
|
||||
domain = Domain.objects.get(name="igorville.gov")
|
||||
self.assertTrue(domain)
|
||||
|
@ -395,7 +398,7 @@ class TestDomainCreation(MockEppLib):
|
|||
|
||||
def tearDown(self) -> None:
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
PublicContact.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
|
|
@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class TestProcessedMigrations(TestCase):
|
||||
"""This test case class is designed to verify the idempotency of migrations
|
||||
related to domain transitions in the application."""
|
||||
related to domain transitions in the domain_request."""
|
||||
|
||||
def setUp(self):
|
||||
"""Defines the file name of migration_json and the folder its contained in"""
|
||||
|
|
|
@ -5,7 +5,7 @@ from .common import MockEppLib # type: ignore
|
|||
|
||||
|
||||
from registrar.models import (
|
||||
DomainApplication,
|
||||
DomainRequest,
|
||||
DomainInformation,
|
||||
)
|
||||
import logging
|
||||
|
@ -26,8 +26,8 @@ class TestViews(TestCase):
|
|||
response = self.client.get("/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_application_form_not_logged_in(self):
|
||||
"""Application form not accessible without a logged-in user."""
|
||||
def test_domain_request_form_not_logged_in(self):
|
||||
"""Domain request form not accessible without a logged-in user."""
|
||||
response = self.client.get("/request/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/login?next=/request/", response.headers["Location"])
|
||||
|
@ -45,9 +45,9 @@ class TestWithUser(MockEppLib):
|
|||
)
|
||||
|
||||
def tearDown(self):
|
||||
# delete any applications too
|
||||
# delete any domain requests too
|
||||
super().tearDown()
|
||||
DomainApplication.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
self.user.delete()
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -21,7 +21,7 @@ from registrar.utility.errors import (
|
|||
)
|
||||
|
||||
from registrar.models import (
|
||||
DomainApplication,
|
||||
DomainRequest,
|
||||
Domain,
|
||||
DomainInformation,
|
||||
DomainInvitation,
|
||||
|
@ -120,7 +120,7 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
UserDomainRole.objects.all().delete()
|
||||
if hasattr(self.domain, "contacts"):
|
||||
self.domain.contacts.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
PublicContact.objects.all().delete()
|
||||
HostIP.objects.all().delete()
|
||||
|
@ -309,9 +309,9 @@ class TestDomainDetail(TestDomainOverview):
|
|||
self.assertContains(detail_page, "(1.2.3.4,")
|
||||
self.assertContains(detail_page, "2.3.4.5)")
|
||||
|
||||
def test_domain_detail_with_no_information_or_application(self):
|
||||
def test_domain_detail_with_no_information_or_domain_request(self):
|
||||
"""Test that domain management page returns 200 and displays error
|
||||
when no domain information or domain application exist"""
|
||||
when no domain information or domain request exist"""
|
||||
with less_console_noise():
|
||||
# have to use staff user for this test
|
||||
staff_user = create_user()
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
import boto3
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.template.loader import get_template
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -15,7 +19,14 @@ class EmailSendingError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}):
|
||||
def send_templated_email(
|
||||
template_name: str,
|
||||
subject_template_name: str,
|
||||
to_address: str,
|
||||
bcc_address="",
|
||||
context={},
|
||||
attachment_file: str = None,
|
||||
):
|
||||
"""Send an email built from a template to one email address.
|
||||
|
||||
template_name and subject_template_name are relative to the same template
|
||||
|
@ -45,15 +56,50 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
|
|||
destination["BccAddresses"] = [bcc_address]
|
||||
|
||||
try:
|
||||
ses_client.send_email(
|
||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||
Destination=destination,
|
||||
Content={
|
||||
"Simple": {
|
||||
"Subject": {"Data": subject},
|
||||
"Body": {"Text": {"Data": email_body}},
|
||||
if attachment_file is None:
|
||||
ses_client.send_email(
|
||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||
Destination=destination,
|
||||
Content={
|
||||
"Simple": {
|
||||
"Subject": {"Data": subject},
|
||||
"Body": {"Text": {"Data": email_body}},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
ses_client = boto3.client(
|
||||
"ses",
|
||||
region_name=settings.AWS_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=settings.BOTO_CONFIG,
|
||||
)
|
||||
send_email_with_attachment(
|
||||
settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, attachment_file, ses_client
|
||||
)
|
||||
except Exception as exc:
|
||||
raise EmailSendingError("Could not send SES email.") from exc
|
||||
|
||||
|
||||
def send_email_with_attachment(sender, recipient, subject, body, attachment_file, ses_client):
|
||||
# Create a multipart/mixed parent container
|
||||
msg = MIMEMultipart("mixed")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = sender
|
||||
msg["To"] = recipient
|
||||
|
||||
# Add the text part
|
||||
text_part = MIMEText(body, "plain")
|
||||
msg.attach(text_part)
|
||||
|
||||
# Add the attachment part
|
||||
attachment_part = MIMEApplication(attachment_file)
|
||||
# Adding attachment header + filename that the attachment will be called
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.zip"
|
||||
attachment_part.add_header("Content-Disposition", f'attachment; filename="{current_filename}"')
|
||||
msg.attach(attachment_part)
|
||||
|
||||
response = ses_client.send_raw_email(Source=sender, Destinations=[recipient], RawMessage={"Data": msg.as_string()})
|
||||
return response
|
||||
|
|
|
@ -71,6 +71,48 @@ class GenericError(Exception):
|
|||
return self._error_mapping.get(code)
|
||||
|
||||
|
||||
class FSMErrorCodes(IntEnum):
|
||||
"""Used when doing FSM transitions.
|
||||
Overview of generic error codes:
|
||||
- 1 APPROVE_DOMAIN_IN_USE The domain is already in use
|
||||
- 2 NO_INVESTIGATOR No investigator is assigned
|
||||
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
|
||||
- 4 INVESTIGATOR_NOT_SUBMITTER The form submitter is not the investigator
|
||||
"""
|
||||
|
||||
APPROVE_DOMAIN_IN_USE = 1
|
||||
NO_INVESTIGATOR = 2
|
||||
INVESTIGATOR_NOT_STAFF = 3
|
||||
INVESTIGATOR_NOT_SUBMITTER = 4
|
||||
|
||||
|
||||
class FSMApplicationError(Exception):
|
||||
"""
|
||||
Used to raise exceptions when doing FSM Transitions.
|
||||
Uses `FSMErrorCodes` as an enum.
|
||||
"""
|
||||
|
||||
_error_mapping = {
|
||||
FSMErrorCodes.APPROVE_DOMAIN_IN_USE: ("Cannot approve. Requested domain is already in use."),
|
||||
FSMErrorCodes.NO_INVESTIGATOR: ("Investigator is required for this status."),
|
||||
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
|
||||
FSMErrorCodes.INVESTIGATOR_NOT_SUBMITTER: ("Only the assigned investigator can make this change."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code = code
|
||||
if self.code in self._error_mapping:
|
||||
self.message = self._error_mapping.get(self.code)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.message}"
|
||||
|
||||
@classmethod
|
||||
def get_error_message(cls, code=None):
|
||||
return cls._error_mapping.get(code)
|
||||
|
||||
|
||||
class NameserverErrorCodes(IntEnum):
|
||||
"""Used in the NameserverError class for
|
||||
error mapping.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue