mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-13 13:09:41 +02:00
Merge remote-tracking branch 'origin/main' into ms/2307-send-notification-emails
This commit is contained in:
commit
859da464b9
94 changed files with 4002 additions and 2076 deletions
|
@ -38,7 +38,7 @@ jobs:
|
||||||
id: check
|
id: check
|
||||||
uses: victoriadrake/django-security-check@master
|
uses: victoriadrake/django-security-check@master
|
||||||
- name: Upload output
|
- name: Upload output
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: security-check-output
|
name: security-check-output
|
||||||
path: output.txt
|
path: output.txt
|
||||||
|
|
1
.github/workflows/deploy-manual.yaml
vendored
1
.github/workflows/deploy-manual.yaml
vendored
|
@ -30,6 +30,7 @@ on:
|
||||||
- litterbox
|
- litterbox
|
||||||
- ms
|
- ms
|
||||||
- ad
|
- ad
|
||||||
|
- ag
|
||||||
# GitHub Actions has no "good" way yet to dynamically input branches
|
# GitHub Actions has no "good" way yet to dynamically input branches
|
||||||
branch:
|
branch:
|
||||||
description: 'Branch to deploy'
|
description: 'Branch to deploy'
|
||||||
|
|
2
.github/workflows/security-check.yaml
vendored
2
.github/workflows/security-check.yaml
vendored
|
@ -44,7 +44,7 @@ jobs:
|
||||||
id: check
|
id: check
|
||||||
uses: ./.github/actions/django-security-check
|
uses: ./.github/actions/django-security-check
|
||||||
- name: Upload output
|
- name: Upload output
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: security-check-output
|
name: security-check-output
|
||||||
path: ./src/output.txt
|
path: ./src/output.txt
|
||||||
|
|
|
@ -42,7 +42,6 @@ class DomainRequest {
|
||||||
creator (User)
|
creator (User)
|
||||||
investigator (User)
|
investigator (User)
|
||||||
senior_official (Contact)
|
senior_official (Contact)
|
||||||
submitter (Contact)
|
|
||||||
other_contacts (Contacts)
|
other_contacts (Contacts)
|
||||||
approved_domain (Domain)
|
approved_domain (Domain)
|
||||||
requested_domain (DraftDomain)
|
requested_domain (DraftDomain)
|
||||||
|
@ -80,7 +79,7 @@ class Contact {
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
|
|
||||||
DomainRequest *-r-* Contact : senior_official, submitter, other_contacts
|
DomainRequest *-r-* Contact : senior_official, other_contacts
|
||||||
|
|
||||||
class DraftDomain {
|
class DraftDomain {
|
||||||
Requested domain
|
Requested domain
|
||||||
|
|
|
@ -38,7 +38,6 @@ class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
|
||||||
+ id (BigAutoField)
|
+ id (BigAutoField)
|
||||||
+ created_at (DateTimeField)
|
+ created_at (DateTimeField)
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
~ user (OneToOneField)
|
|
||||||
+ first_name (CharField)
|
+ first_name (CharField)
|
||||||
+ middle_name (CharField)
|
+ middle_name (CharField)
|
||||||
+ last_name (CharField)
|
+ last_name (CharField)
|
||||||
|
@ -47,7 +46,6 @@ class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
|
||||||
+ phone (PhoneNumberField)
|
+ phone (PhoneNumberField)
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
registrar.Contact -- registrar.User
|
|
||||||
|
|
||||||
|
|
||||||
class "registrar.Host <Registrar>" as registrar.Host #d6f4e9 {
|
class "registrar.Host <Registrar>" as registrar.Host #d6f4e9 {
|
||||||
|
@ -143,6 +141,8 @@ class "registrar.FederalAgency <Registrar>" as registrar.FederalAgency #d6f4e9 {
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
+ agency (CharField)
|
+ agency (CharField)
|
||||||
+ federal_type (CharField)
|
+ federal_type (CharField)
|
||||||
|
+ initials (CharField)
|
||||||
|
+ is_fceb (BooleanField)
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,6 +159,7 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
||||||
+ action_needed_reason_email (TextField)
|
+ action_needed_reason_email (TextField)
|
||||||
~ federal_agency (ForeignKey)
|
~ federal_agency (ForeignKey)
|
||||||
~ portfolio (ForeignKey)
|
~ portfolio (ForeignKey)
|
||||||
|
~ sub_organization (ForeignKey)
|
||||||
~ creator (ForeignKey)
|
~ creator (ForeignKey)
|
||||||
~ investigator (ForeignKey)
|
~ investigator (ForeignKey)
|
||||||
+ generic_org_type (CharField)
|
+ generic_org_type (CharField)
|
||||||
|
@ -179,7 +180,6 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
||||||
~ senior_official (ForeignKey)
|
~ senior_official (ForeignKey)
|
||||||
~ approved_domain (OneToOneField)
|
~ approved_domain (OneToOneField)
|
||||||
~ requested_domain (OneToOneField)
|
~ requested_domain (OneToOneField)
|
||||||
~ submitter (ForeignKey)
|
|
||||||
+ purpose (TextField)
|
+ purpose (TextField)
|
||||||
+ no_other_contacts_rationale (TextField)
|
+ no_other_contacts_rationale (TextField)
|
||||||
+ anything_else (TextField)
|
+ anything_else (TextField)
|
||||||
|
@ -198,12 +198,12 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
||||||
}
|
}
|
||||||
registrar.DomainRequest -- registrar.FederalAgency
|
registrar.DomainRequest -- registrar.FederalAgency
|
||||||
registrar.DomainRequest -- registrar.Portfolio
|
registrar.DomainRequest -- registrar.Portfolio
|
||||||
|
registrar.DomainRequest -- registrar.Suborganization
|
||||||
registrar.DomainRequest -- registrar.User
|
registrar.DomainRequest -- registrar.User
|
||||||
registrar.DomainRequest -- registrar.User
|
registrar.DomainRequest -- registrar.User
|
||||||
registrar.DomainRequest -- registrar.Contact
|
registrar.DomainRequest -- registrar.Contact
|
||||||
registrar.DomainRequest -- registrar.Domain
|
registrar.DomainRequest -- registrar.Domain
|
||||||
registrar.DomainRequest -- registrar.DraftDomain
|
registrar.DomainRequest -- registrar.DraftDomain
|
||||||
registrar.DomainRequest -- registrar.Contact
|
|
||||||
registrar.DomainRequest *--* registrar.Website
|
registrar.DomainRequest *--* registrar.Website
|
||||||
registrar.DomainRequest *--* registrar.Website
|
registrar.DomainRequest *--* registrar.Website
|
||||||
registrar.DomainRequest *--* registrar.Contact
|
registrar.DomainRequest *--* registrar.Contact
|
||||||
|
@ -218,6 +218,7 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
|
||||||
~ federal_agency (ForeignKey)
|
~ federal_agency (ForeignKey)
|
||||||
~ creator (ForeignKey)
|
~ creator (ForeignKey)
|
||||||
~ portfolio (ForeignKey)
|
~ portfolio (ForeignKey)
|
||||||
|
~ sub_organization (ForeignKey)
|
||||||
~ domain_request (OneToOneField)
|
~ domain_request (OneToOneField)
|
||||||
+ generic_org_type (CharField)
|
+ generic_org_type (CharField)
|
||||||
+ organization_type (CharField)
|
+ organization_type (CharField)
|
||||||
|
@ -236,7 +237,6 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
|
||||||
+ about_your_organization (TextField)
|
+ about_your_organization (TextField)
|
||||||
~ senior_official (ForeignKey)
|
~ senior_official (ForeignKey)
|
||||||
~ domain (OneToOneField)
|
~ domain (OneToOneField)
|
||||||
~ submitter (ForeignKey)
|
|
||||||
+ purpose (TextField)
|
+ purpose (TextField)
|
||||||
+ no_other_contacts_rationale (TextField)
|
+ no_other_contacts_rationale (TextField)
|
||||||
+ anything_else (TextField)
|
+ anything_else (TextField)
|
||||||
|
@ -253,10 +253,10 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
|
||||||
registrar.DomainInformation -- registrar.FederalAgency
|
registrar.DomainInformation -- registrar.FederalAgency
|
||||||
registrar.DomainInformation -- registrar.User
|
registrar.DomainInformation -- registrar.User
|
||||||
registrar.DomainInformation -- registrar.Portfolio
|
registrar.DomainInformation -- registrar.Portfolio
|
||||||
|
registrar.DomainInformation -- registrar.Suborganization
|
||||||
registrar.DomainInformation -- registrar.DomainRequest
|
registrar.DomainInformation -- registrar.DomainRequest
|
||||||
registrar.DomainInformation -- registrar.Contact
|
registrar.DomainInformation -- registrar.Contact
|
||||||
registrar.DomainInformation -- registrar.Domain
|
registrar.DomainInformation -- registrar.Domain
|
||||||
registrar.DomainInformation -- registrar.Contact
|
|
||||||
registrar.DomainInformation *--* registrar.Contact
|
registrar.DomainInformation *--* registrar.Contact
|
||||||
|
|
||||||
|
|
||||||
|
@ -285,6 +285,38 @@ class "registrar.DomainInvitation <Registrar>" as registrar.DomainInvitation #d6
|
||||||
registrar.DomainInvitation -- registrar.Domain
|
registrar.DomainInvitation -- registrar.Domain
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.UserPortfolioPermission <Registrar>" as registrar.UserPortfolioPermission #d6f4e9 {
|
||||||
|
user portfolio permission
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
~ user (ForeignKey)
|
||||||
|
~ portfolio (ForeignKey)
|
||||||
|
+ roles (ArrayField)
|
||||||
|
+ additional_permissions (ArrayField)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
registrar.UserPortfolioPermission -- registrar.User
|
||||||
|
registrar.UserPortfolioPermission -- registrar.Portfolio
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.PortfolioInvitation <Registrar>" as registrar.PortfolioInvitation #d6f4e9 {
|
||||||
|
portfolio invitation
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
+ email (EmailField)
|
||||||
|
~ portfolio (ForeignKey)
|
||||||
|
+ portfolio_roles (ArrayField)
|
||||||
|
+ portfolio_additional_permissions (ArrayField)
|
||||||
|
+ status (FSMField)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
registrar.PortfolioInvitation -- registrar.Portfolio
|
||||||
|
|
||||||
|
|
||||||
class "registrar.TransitionDomain <Registrar>" as registrar.TransitionDomain #d6f4e9 {
|
class "registrar.TransitionDomain <Registrar>" as registrar.TransitionDomain #d6f4e9 {
|
||||||
transition domain
|
transition domain
|
||||||
--
|
--
|
||||||
|
@ -409,10 +441,11 @@ class "registrar.Portfolio <Registrar>" as registrar.Portfolio #d6f4e9 {
|
||||||
+ created_at (DateTimeField)
|
+ created_at (DateTimeField)
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
~ creator (ForeignKey)
|
~ creator (ForeignKey)
|
||||||
|
+ organization_name (CharField)
|
||||||
|
+ organization_type (CharField)
|
||||||
+ notes (TextField)
|
+ notes (TextField)
|
||||||
~ federal_agency (ForeignKey)
|
~ federal_agency (ForeignKey)
|
||||||
+ organization_type (CharField)
|
~ senior_official (ForeignKey)
|
||||||
+ organization_name (CharField)
|
|
||||||
+ address_line1 (CharField)
|
+ address_line1 (CharField)
|
||||||
+ address_line2 (CharField)
|
+ address_line2 (CharField)
|
||||||
+ city (CharField)
|
+ city (CharField)
|
||||||
|
@ -424,6 +457,7 @@ class "registrar.Portfolio <Registrar>" as registrar.Portfolio #d6f4e9 {
|
||||||
}
|
}
|
||||||
registrar.Portfolio -- registrar.User
|
registrar.Portfolio -- registrar.User
|
||||||
registrar.Portfolio -- registrar.FederalAgency
|
registrar.Portfolio -- registrar.FederalAgency
|
||||||
|
registrar.Portfolio -- registrar.SeniorOfficial
|
||||||
|
|
||||||
|
|
||||||
class "registrar.DomainGroup <Registrar>" as registrar.DomainGroup #d6f4e9 {
|
class "registrar.DomainGroup <Registrar>" as registrar.DomainGroup #d6f4e9 {
|
||||||
|
@ -454,7 +488,21 @@ class "registrar.Suborganization <Registrar>" as registrar.Suborganization #d6f4
|
||||||
registrar.Suborganization -- registrar.Portfolio
|
registrar.Suborganization -- registrar.Portfolio
|
||||||
|
|
||||||
|
|
||||||
@enduml
|
class "registrar.SeniorOfficial <Registrar>" as registrar.SeniorOfficial #d6f4e9 {
|
||||||
```
|
senior official
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
+ first_name (CharField)
|
||||||
|
+ last_name (CharField)
|
||||||
|
+ title (CharField)
|
||||||
|
+ phone (PhoneNumberField)
|
||||||
|
+ email (EmailField)
|
||||||
|
~ federal_agency (ForeignKey)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
registrar.SeniorOfficial -- registrar.FederalAgency
|
||||||
|
|
||||||
</details>
|
|
||||||
|
@enduml
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 130 KiB |
|
@ -9,10 +9,10 @@ We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature
|
||||||
3. Click `Add waffle flag`.
|
3. Click `Add waffle flag`.
|
||||||
4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them.
|
4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them.
|
||||||
|
|
||||||
### Enabling the profile_feature flag
|
### Enabling a feature flag
|
||||||
1. On the app, navigate to `\admin`.
|
1. On the app, navigate to `\admin`.
|
||||||
2. Under models, click `Waffle flags`.
|
2. Under models, click `Waffle flags`.
|
||||||
3. Click the `profile_feature` record. This should exist by default, if not - create one with that name.
|
3. Click the featue flag record. This should exist by default, if not - create one with that name.
|
||||||
4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else.
|
4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else.
|
||||||
5. Configure the settings as you see fit.
|
5. Configure the settings as you see fit.
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
- Starting Location: Home page
|
- Starting Location: Home page
|
||||||
- Workflow: (Domain requests Table) Manage domain
|
- Workflow: (Domain requests Table) Manage domain
|
||||||
- Workflow Step: Click "Manage" -> Click "Withdraw request" -> (confirmation prompt) -> Click "Withdraw request" (inside prompt)
|
- Workflow Step: Click "Manage" -> Click "Withdraw request" -> (confirmation prompt) -> Click "Withdraw request" (inside prompt)
|
||||||
- Notes: You can also do this through Django Admin by switching a domain of status "submitted" to "withdrawn", but you need to be the submitter (email listed on Your Contact Information).
|
- Notes: You can also do this through Django Admin by switching a domain of status "submitted" to "withdrawn", but you need to be the creator.
|
||||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn.txt)
|
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn.txt)
|
||||||
|
|
||||||
### Domain Request Withdrawn Subject
|
### Domain Request Withdrawn Subject
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
- Starting Location: Django Admin
|
- Starting Location: Django Admin
|
||||||
- Workflow: Analyst Admin
|
- Workflow: Analyst Admin
|
||||||
- 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"
|
- 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.
|
- Notes: Note that this will send an email to the creator. 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)
|
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_approved.txt)
|
||||||
|
|
||||||
### Status Change Approved Subject
|
### Status Change Approved Subject
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
- Starting Location: Django Admin
|
- Starting Location: Django Admin
|
||||||
- Workflow: Analyst Admin
|
- Workflow: Analyst Admin
|
||||||
- Workflow Step: Click "domain requests" -> Click a domain request in a status of "In review", or "approved" -> Click status dropdown -> (select "rejected") -> click "Save"
|
- 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.
|
- Notes: Note that this will send an email to the creator. 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)
|
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_rejected.txt)
|
||||||
|
|
||||||
### Status Change Rejected Subject
|
### Status Change Rejected Subject
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
- Starting Location: Home Page
|
- Starting Location: Home Page
|
||||||
- Workflow: Start domain request
|
- Workflow: Start domain request
|
||||||
- Workflow Step: Click "Start a new domain request" -> (fill out the form) -> On the last step ("Review and submit your domain request "), click "Submit your domain request"
|
- Workflow Step: Click "Start a new domain request" -> (fill out the form) -> On the last step ("Review and submit your domain request "), click "Submit your domain request"
|
||||||
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information)
|
- Notes: Note that this will send an email to the creator.
|
||||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/submission_confirmation.txt)
|
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/submission_confirmation.txt)
|
||||||
|
|
||||||
### Submission Confirmation Subject
|
### Submission Confirmation Subject
|
||||||
|
|
|
@ -860,3 +860,55 @@ Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
### Running locally
|
### Running locally
|
||||||
```docker-compose exec app ./manage.py populate_domain_request_dates```
|
```docker-compose exec app ./manage.py populate_domain_request_dates```
|
||||||
|
|
||||||
|
## Create federal portfolio
|
||||||
|
This script takes the name of a `FederalAgency` (like 'AMTRAK') and does the following:
|
||||||
|
1. Creates the portfolio record based off of data on the federal agency object itself.
|
||||||
|
2. Creates suborganizations from existing DomainInformation records.
|
||||||
|
3. Associates the SeniorOfficial record (if it exists).
|
||||||
|
4. Adds this portfolio to DomainInformation / DomainRequests or both.
|
||||||
|
|
||||||
|
Errors:
|
||||||
|
1. ValueError: Federal agency not found in database.
|
||||||
|
2. Logged Warning: No senior official found for portfolio
|
||||||
|
3. Logged Error: No suborganizations found for portfolio.
|
||||||
|
4. Logged Warning: No new suborganizations to add.
|
||||||
|
5. Logged Warning: No valid DomainRequest records to update.
|
||||||
|
6. Logged Warning: No valid DomainInformation records to update.
|
||||||
|
|
||||||
|
### Running on sandboxes
|
||||||
|
|
||||||
|
#### Step 1: Login to CloudFoundry
|
||||||
|
```cf login -a api.fr.cloud.gov --sso```
|
||||||
|
|
||||||
|
#### Step 2: SSH into your environment
|
||||||
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
|
Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
|
#### Step 3: Create a shell instance
|
||||||
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
|
#### Step 4: Upload your csv to the desired sandbox
|
||||||
|
[Follow these steps](#use-scp-to-transfer-data-to-sandboxes) to upload the federal_cio csv to a sandbox of your choice.
|
||||||
|
|
||||||
|
#### Step 5: Running the script
|
||||||
|
```./manage.py create_federal_portfolio "{federal_agency_name}" --both```
|
||||||
|
|
||||||
|
Example (only requests): `./manage.py create_federal_portfolio "AMTRAK" --parse_requests`
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
|
||||||
|
#### Step 1: Running the script
|
||||||
|
```docker-compose exec app ./manage.py create_federal_portfolio "{federal_agency_name}" --both```
|
||||||
|
|
||||||
|
##### Parameters
|
||||||
|
| | Parameter | Description |
|
||||||
|
|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------|
|
||||||
|
| 1 | **federal_agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
||||||
|
| 2 | **both** | If True, runs parse_requests and parse_domains. |
|
||||||
|
| 3 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
||||||
|
| 4 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
||||||
|
|
||||||
|
Note: Regarding parameters #2-#3, you cannot use `--both` while using these. You must specify either `--parse_requests` or `--parse_domains` seperately. While all of these parameters are optional in that you do not need to specify all of them,
|
||||||
|
you must specify at least one to run this script.
|
||||||
|
|
|
@ -126,7 +126,15 @@ class AvailableAPITest(MockEppLib):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = get_user_model().objects.create(username="username")
|
username = "test_user"
|
||||||
|
first_name = "First"
|
||||||
|
last_name = "Last"
|
||||||
|
email = "info@example.com"
|
||||||
|
title = "title"
|
||||||
|
phone = "8080102431"
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone
|
||||||
|
)
|
||||||
|
|
||||||
def test_available_get(self):
|
def test_available_get(self):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Cert:
|
class Cert:
|
||||||
|
@ -12,7 +12,7 @@ class Cert:
|
||||||
variable but Python's ssl library requires a file.
|
variable but Python's ssl library requires a file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None:
|
def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None: # type: ignore
|
||||||
self.filename = self._write(data)
|
self.filename = self._write(data)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
@ -31,4 +31,4 @@ class Key(Cert):
|
||||||
"""Location of private key as written to disk."""
|
"""Location of private key as written to disk."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(data=settings.SECRET_REGISTRY_KEY)
|
super().__init__(data=settings.SECRET_REGISTRY_KEY) # type: ignore
|
||||||
|
|
|
@ -536,7 +536,6 @@ class AdminSortFields:
|
||||||
sort_mapping = {
|
sort_mapping = {
|
||||||
# == Contact == #
|
# == Contact == #
|
||||||
"other_contacts": (Contact, _name_sort),
|
"other_contacts": (Contact, _name_sort),
|
||||||
"submitter": (Contact, _name_sort),
|
|
||||||
# == Senior Official == #
|
# == Senior Official == #
|
||||||
"senior_official": (SeniorOfficial, _name_sort),
|
"senior_official": (SeniorOfficial, _name_sort),
|
||||||
# == User == #
|
# == User == #
|
||||||
|
@ -963,7 +962,9 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||||
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
|
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
|
||||||
|
|
||||||
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True)
|
||||||
|
portfolios = models.Portfolio.objects.filter(id__in=portfolio_ids)
|
||||||
|
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1441,13 +1442,9 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"domain",
|
"domain",
|
||||||
"generic_org_type",
|
"generic_org_type",
|
||||||
"created_at",
|
"created_at",
|
||||||
"submitter",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
orderable_fk_fields = [
|
orderable_fk_fields = [("domain", "name")]
|
||||||
("domain", "name"),
|
|
||||||
("submitter", ["first_name", "last_name"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = ["generic_org_type"]
|
list_filter = ["generic_org_type"]
|
||||||
|
@ -1459,7 +1456,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
search_help_text = "Search by domain."
|
search_help_text = "Search by domain."
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["portfolio", "sub_organization", "creator", "submitter", "domain_request", "notes"]}),
|
(None, {"fields": ["portfolio", "sub_organization", "creator", "domain_request", "notes"]}),
|
||||||
(".gov domain", {"fields": ["domain"]}),
|
(".gov domain", {"fields": ["domain"]}),
|
||||||
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
|
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||||
("Background info", {"fields": ["anything_else"]}),
|
("Background info", {"fields": ["anything_else"]}),
|
||||||
|
@ -1523,7 +1520,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"more_organization_information",
|
"more_organization_information",
|
||||||
"domain",
|
"domain",
|
||||||
"domain_request",
|
"domain_request",
|
||||||
"submitter",
|
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
@ -1538,7 +1534,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"domain_request",
|
"domain_request",
|
||||||
"senior_official",
|
"senior_official",
|
||||||
"domain",
|
"domain",
|
||||||
"submitter",
|
|
||||||
"portfolio",
|
"portfolio",
|
||||||
"sub_organization",
|
"sub_organization",
|
||||||
]
|
]
|
||||||
|
@ -1711,13 +1706,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"custom_election_board",
|
"custom_election_board",
|
||||||
"city",
|
"city",
|
||||||
"state_territory",
|
"state_territory",
|
||||||
"submitter",
|
|
||||||
"investigator",
|
"investigator",
|
||||||
]
|
]
|
||||||
|
|
||||||
orderable_fk_fields = [
|
orderable_fk_fields = [
|
||||||
("requested_domain", "name"),
|
("requested_domain", "name"),
|
||||||
("submitter", ["first_name", "last_name"]),
|
|
||||||
("investigator", ["first_name", "last_name"]),
|
("investigator", ["first_name", "last_name"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1747,11 +1740,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# Search
|
# Search
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"requested_domain__name",
|
"requested_domain__name",
|
||||||
"submitter__email",
|
"creator__email",
|
||||||
"submitter__first_name",
|
"creator__first_name",
|
||||||
"submitter__last_name",
|
"creator__last_name",
|
||||||
]
|
]
|
||||||
search_help_text = "Search by domain or submitter."
|
search_help_text = "Search by domain or creator."
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(
|
(
|
||||||
|
@ -1767,7 +1760,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"action_needed_reason_email",
|
"action_needed_reason_email",
|
||||||
"investigator",
|
"investigator",
|
||||||
"creator",
|
"creator",
|
||||||
"submitter",
|
|
||||||
"approved_domain",
|
"approved_domain",
|
||||||
"notes",
|
"notes",
|
||||||
]
|
]
|
||||||
|
@ -1855,7 +1847,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"approved_domain",
|
"approved_domain",
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"purpose",
|
"purpose",
|
||||||
"submitter",
|
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
@ -1866,7 +1857,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
"approved_domain",
|
"approved_domain",
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
"submitter",
|
|
||||||
"creator",
|
"creator",
|
||||||
"senior_official",
|
"senior_official",
|
||||||
"investigator",
|
"investigator",
|
||||||
|
@ -1987,15 +1977,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
so we should display that information using this function.
|
so we should display that information using this function.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
recipient = obj.creator
|
||||||
# TODO 2574: remove lines 1977-1978 (refactor as needed)
|
|
||||||
profile_flag = flag_is_active(request, "profile_feature")
|
|
||||||
if profile_flag and hasattr(obj, "creator"):
|
|
||||||
recipient = obj.creator
|
|
||||||
elif not profile_flag and hasattr(obj, "submitter"):
|
|
||||||
recipient = obj.submitter
|
|
||||||
else:
|
|
||||||
recipient = None
|
|
||||||
|
|
||||||
# Displays a warning in admin when an email cannot be sent
|
# Displays a warning in admin when an email cannot be sent
|
||||||
if recipient and recipient.email:
|
if recipient and recipient.email:
|
||||||
|
@ -2200,7 +2182,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
||||||
emails = self.get_all_action_needed_reason_emails(obj)
|
emails = self.get_all_action_needed_reason_emails(obj)
|
||||||
extra_context["action_needed_reason_emails"] = json.dumps(emails)
|
extra_context["action_needed_reason_emails"] = json.dumps(emails)
|
||||||
extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
|
||||||
|
|
||||||
# Denote if an action needed email was sent or not
|
# Denote if an action needed email was sent or not
|
||||||
email_sent = request.session.get("action_needed_email_sent", False)
|
email_sent = request.session.get("action_needed_email_sent", False)
|
||||||
|
@ -2229,10 +2210,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
|
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if flag_is_active(None, "profile_feature"): # type: ignore
|
recipient = domain_request.creator
|
||||||
recipient = domain_request.creator
|
|
||||||
else:
|
|
||||||
recipient = domain_request.submitter
|
|
||||||
|
|
||||||
# Return the context of the rendered views
|
# Return the context of the rendered views
|
||||||
context = {"domain_request": domain_request, "recipient": recipient}
|
context = {"domain_request": domain_request, "recipient": recipient}
|
||||||
|
|
|
@ -748,7 +748,10 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
|
|
||||||
//------ Requested Domains
|
//------ Requested Domains
|
||||||
const requestedDomainElement = document.getElementById('id_requested_domain');
|
const requestedDomainElement = document.getElementById('id_requested_domain');
|
||||||
const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text;
|
// We have to account for different superuser and analyst markups
|
||||||
|
const requestedDomain = requestedDomainElement.options
|
||||||
|
? requestedDomainElement.options[requestedDomainElement.selectedIndex].text
|
||||||
|
: requestedDomainElement.text;
|
||||||
|
|
||||||
//------ Submitter
|
//------ Submitter
|
||||||
// Function to extract text by ID and handle missing elements
|
// Function to extract text by ID and handle missing elements
|
||||||
|
@ -762,7 +765,10 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
// Extract the submitter name, title, email, and phone number
|
// Extract the submitter name, title, email, and phone number
|
||||||
const submitterDiv = document.querySelector('.form-row.field-submitter');
|
const submitterDiv = document.querySelector('.form-row.field-submitter');
|
||||||
const submitterNameElement = document.getElementById('id_submitter');
|
const submitterNameElement = document.getElementById('id_submitter');
|
||||||
const submitterName = submitterNameElement.options[submitterNameElement.selectedIndex].text;
|
// We have to account for different superuser and analyst markups
|
||||||
|
const submitterName = submitterNameElement
|
||||||
|
? submitterNameElement.options[submitterNameElement.selectedIndex].text
|
||||||
|
: submitterDiv.querySelector('a').text;
|
||||||
const submitterTitle = extractTextById('contact_info_title', submitterDiv);
|
const submitterTitle = extractTextById('contact_info_title', submitterDiv);
|
||||||
const submitterEmail = extractTextById('contact_info_email', submitterDiv);
|
const submitterEmail = extractTextById('contact_info_email', submitterDiv);
|
||||||
const submitterPhone = extractTextById('contact_info_phone', submitterDiv);
|
const submitterPhone = extractTextById('contact_info_phone', submitterDiv);
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,7 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
.usa-accordion--select {
|
.usa-accordion--select,
|
||||||
|
.usa-accordion--more-actions {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: auto;
|
width: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -14,7 +15,6 @@
|
||||||
// Note, width is determined by a custom width class on one of the children
|
// Note, width is determined by a custom width class on one of the children
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 33.88px;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px color('base-lighter');
|
border: solid 1px color('base-lighter');
|
||||||
|
@ -31,3 +31,17 @@
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-accordion--select .usa-accordion__content {
|
||||||
|
top: 33.88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-accordion--more-actions .usa-accordion__content {
|
||||||
|
top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child .usa-accordion--more-actions .usa-accordion__content {
|
||||||
|
top: auto;
|
||||||
|
bottom: -10px;
|
||||||
|
right: 30px;
|
||||||
|
}
|
||||||
|
|
|
@ -159,6 +159,23 @@ abbr[title] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden-mobile-flex {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
.visible-mobile-flex {
|
||||||
|
display: flex!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include at-media(tablet) {
|
||||||
|
.hidden-mobile-flex {
|
||||||
|
display: flex!important;
|
||||||
|
}
|
||||||
|
.visible-mobile-flex {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.flex-end {
|
.flex-end {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
@ -200,6 +217,11 @@ abbr[title] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-right-neg-4px {
|
// Boost this USWDS utility class for the accordions in the portfolio requests table
|
||||||
margin-right: -4px;
|
.left-auto {
|
||||||
|
left: auto!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-word {
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
@use "cisa_colors" as *;
|
@use "cisa_colors" as *;
|
||||||
|
|
||||||
/* Make "placeholder" links visually obvious */
|
// Used on: TODO links
|
||||||
|
// Used on: NONE
|
||||||
a[href$="todo"]::after {
|
a[href$="todo"]::after {
|
||||||
background-color: yellow;
|
background-color: yellow;
|
||||||
color: color(blue-80v);
|
color: color(blue-80v);
|
||||||
|
@ -9,10 +10,14 @@ a[href$="todo"]::after {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used on: profile
|
||||||
|
// Note: Is this needed?
|
||||||
a.usa-link.usa-link--always-blue {
|
a.usa-link.usa-link--always-blue {
|
||||||
color: #{$dhs-blue};
|
color: #{$dhs-blue};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used on: breadcrumbs
|
||||||
|
// Note: This could potentially be simplified and use usa-button--with-icon
|
||||||
a.breadcrumb__back {
|
a.breadcrumb__back {
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -28,10 +33,18 @@ a.breadcrumb__back {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove anchor buttons' underline
|
||||||
a.usa-button {
|
a.usa-button {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unstyled anchor buttons
|
||||||
|
a.usa-button--unstyled:visited {
|
||||||
|
color: color('primary');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled anchor buttons
|
||||||
|
// NOTE: Not used
|
||||||
a.usa-button.disabled-link {
|
a.usa-button.disabled-link {
|
||||||
background-color: #ccc !important;
|
background-color: #ccc !important;
|
||||||
color: #454545 !important
|
color: #454545 !important
|
||||||
|
@ -58,6 +71,8 @@ a.usa-button--unstyled.disabled-link:focus {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disabled buttons
|
||||||
|
// Used on: Domain managers, disabled logo on profile
|
||||||
.usa-button--unstyled.disabled-button,
|
.usa-button--unstyled.disabled-button,
|
||||||
.usa-button--unstyled.disabled-button:hover,
|
.usa-button--unstyled.disabled-button:hover,
|
||||||
.usa-button--unstyled.disabled-button:focus {
|
.usa-button--unstyled.disabled-button:focus {
|
||||||
|
@ -66,6 +81,16 @@ a.usa-button--unstyled.disabled-link:focus {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unstyled variant for reverse out?
|
||||||
|
// Used on: NONE
|
||||||
|
.usa-button--unstyled--white,
|
||||||
|
.usa-button--unstyled--white:hover,
|
||||||
|
.usa-button--unstyled--white:focus,
|
||||||
|
.usa-button--unstyled--white:active {
|
||||||
|
color: color('white');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solid anchor buttons
|
||||||
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
|
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
|
||||||
color: color('white');
|
color: color('white');
|
||||||
}
|
}
|
||||||
|
@ -77,6 +102,7 @@ a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active {
|
||||||
color: color('white');
|
color: color('white');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outline anchor buttons
|
||||||
a.usa-button--outline,
|
a.usa-button--outline,
|
||||||
a.usa-button--outline:visited {
|
a.usa-button--outline:visited {
|
||||||
box-shadow: inset 0 0 0 2px color('primary');
|
box-shadow: inset 0 0 0 2px color('primary');
|
||||||
|
@ -94,10 +120,22 @@ a.usa-button--outline:active {
|
||||||
color: color('primary-darker');
|
color: color('primary-darker');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used on: Domain request withdraw confirmation
|
||||||
a.withdraw {
|
a.withdraw {
|
||||||
background-color: color('error');
|
background-color: color('error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.withdraw:hover,
|
||||||
|
a.withdraw:focus {
|
||||||
|
background-color: color('error-dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
a.withdraw:active {
|
||||||
|
background-color: color('error-darker');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used on: Domain request status
|
||||||
|
//NOTE: Revise to BEM convention usa-button--outline-secondary
|
||||||
a.withdraw_outline,
|
a.withdraw_outline,
|
||||||
a.withdraw_outline:visited {
|
a.withdraw_outline:visited {
|
||||||
box-shadow: inset 0 0 0 2px color('error');
|
box-shadow: inset 0 0 0 2px color('error');
|
||||||
|
@ -115,19 +153,8 @@ a.withdraw_outline:active {
|
||||||
color: color('error-darker');
|
color: color('error-darker');
|
||||||
}
|
}
|
||||||
|
|
||||||
a.withdraw:hover,
|
|
||||||
a.withdraw:focus {
|
|
||||||
background-color: color('error-dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
a.withdraw:active {
|
|
||||||
background-color: color('error-darker');
|
|
||||||
}
|
|
||||||
|
|
||||||
a.usa-button--unstyled:visited {
|
|
||||||
color: color('primary');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Used on: Domain request submit
|
||||||
.dotgov-button--green {
|
.dotgov-button--green {
|
||||||
background-color: color('success-dark');
|
background-color: color('success-dark');
|
||||||
|
|
||||||
|
@ -140,15 +167,8 @@ a.usa-button--unstyled:visited {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-button--unstyled--white,
|
// Cancel button
|
||||||
.usa-button--unstyled--white:hover,
|
// Used on: DNSSEC main page
|
||||||
.usa-button--unstyled--white:focus,
|
|
||||||
.usa-button--unstyled--white:active {
|
|
||||||
color: color('white');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel button used on the
|
|
||||||
// DNSSEC main page
|
|
||||||
// We want to center this button on mobile
|
// We want to center this button on mobile
|
||||||
// and add some extra left margin on tablet+
|
// and add some extra left margin on tablet+
|
||||||
.usa-button--cancel {
|
.usa-button--cancel {
|
||||||
|
@ -175,6 +195,8 @@ a.usa-button--unstyled:visited {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used on: Profile page, toggleable fields
|
||||||
|
// Note: Could potentially be cleaned up by using usa-button--with-icon
|
||||||
// We need to deviate from some default USWDS styles here
|
// We need to deviate from some default USWDS styles here
|
||||||
// in this particular case, so we have to override this.
|
// in this particular case, so we have to override this.
|
||||||
.usa-form .usa-button.readonly-edit-button {
|
.usa-form .usa-button.readonly-edit-button {
|
||||||
|
@ -186,6 +208,7 @@ a.usa-button--unstyled:visited {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Used on: Domains and Requests tables
|
||||||
.usa-button--filter {
|
.usa-button--filter {
|
||||||
width: auto;
|
width: auto;
|
||||||
// For mobile stacking
|
// For mobile stacking
|
||||||
|
@ -201,6 +224,8 @@ a.usa-button--unstyled:visited {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buttons with nested icons
|
||||||
|
// Note: Can be simplified by adding usa-link--icon to anchors in tables
|
||||||
.dotgov-table a,
|
.dotgov-table a,
|
||||||
.usa-link--icon,
|
.usa-link--icon,
|
||||||
.usa-button--with-icon {
|
.usa-button--with-icon {
|
||||||
|
@ -211,14 +236,7 @@ a.usa-button--unstyled:visited {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dotgov-table a
|
||||||
.dotgov-table a,
|
|
||||||
.usa-link--icon {
|
|
||||||
&:visited {
|
|
||||||
color: color('primary');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a .usa-icon,
|
a .usa-icon,
|
||||||
.usa-button--with-icon .usa-icon {
|
.usa-button--with-icon .usa-icon {
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
|
@ -230,3 +248,12 @@ a .usa-icon,
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
width: 1.5em;
|
width: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Red, for delete buttons
|
||||||
|
// Used on: All delete buttons
|
||||||
|
// Note: Can be simplified by adding text-secondary to delete anchors in tables
|
||||||
|
button.text-secondary,
|
||||||
|
button.text-secondary:hover,
|
||||||
|
.dotgov-table a.text-secondary {
|
||||||
|
color: $theme-color-error;
|
||||||
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ legend.float-left-tablet + button.float-right-tablet {
|
||||||
|
|
||||||
.read-only-label {
|
.read-only-label {
|
||||||
font-size: size('body', 'sm');
|
font-size: size('body', 'sm');
|
||||||
color: color('primary');
|
color: color('primary-dark');
|
||||||
margin-bottom: units(0.5);
|
margin-bottom: units(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,16 +89,24 @@
|
||||||
.usa-nav__primary {
|
.usa-nav__primary {
|
||||||
.usa-nav-link,
|
.usa-nav-link,
|
||||||
.usa-nav-link:hover,
|
.usa-nav-link:hover,
|
||||||
.usa-nav-link:active {
|
.usa-nav-link:active,
|
||||||
|
button {
|
||||||
color: color('primary');
|
color: color('primary');
|
||||||
font-weight: font-weight('normal');
|
font-weight: font-weight('normal');
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.usa-current,
|
.usa-current,
|
||||||
.usa-current:hover,
|
.usa-current:hover,
|
||||||
.usa-current:active {
|
.usa-current:active,
|
||||||
|
button.usa-current {
|
||||||
font-weight: font-weight('bold');
|
font-weight: font-weight('bold');
|
||||||
}
|
}
|
||||||
|
button[aria-expanded="true"] {
|
||||||
|
color: color('white');
|
||||||
|
}
|
||||||
|
button:not(.usa-current):hover::after {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.usa-nav__secondary {
|
.usa-nav__secondary {
|
||||||
// I don't know why USWDS has this at 2 rem, which puts it out of alignment
|
// I don't know why USWDS has this at 2 rem, which puts it out of alignment
|
||||||
|
|
|
@ -23,6 +23,9 @@ from cfenv import AppEnv # type: ignore
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from botocore.config import Config
|
from botocore.config import Config
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from django.utils.log import ServerFormatter
|
||||||
|
|
||||||
# # # ###
|
# # # ###
|
||||||
# Setup code goes here #
|
# Setup code goes here #
|
||||||
|
@ -57,7 +60,7 @@ env_db_url = env.dj_db_url("DATABASE_URL")
|
||||||
env_debug = env.bool("DJANGO_DEBUG", default=False)
|
env_debug = env.bool("DJANGO_DEBUG", default=False)
|
||||||
env_is_production = env.bool("IS_PRODUCTION", default=False)
|
env_is_production = env.bool("IS_PRODUCTION", default=False)
|
||||||
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG")
|
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG")
|
||||||
env_base_url = env.str("DJANGO_BASE_URL")
|
env_base_url: str = env.str("DJANGO_BASE_URL")
|
||||||
env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
|
env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
|
||||||
env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox")
|
env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox")
|
||||||
|
|
||||||
|
@ -192,7 +195,7 @@ MIDDLEWARE = [
|
||||||
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
# application object used by Django’s built-in servers (e.g. `runserver`)
|
# application object used by Django's built-in servers (e.g. `runserver`)
|
||||||
WSGI_APPLICATION = "registrar.config.wsgi.application"
|
WSGI_APPLICATION = "registrar.config.wsgi.application"
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -242,7 +245,6 @@ TEMPLATES = [
|
||||||
"registrar.context_processors.is_production",
|
"registrar.context_processors.is_production",
|
||||||
"registrar.context_processors.org_user_status",
|
"registrar.context_processors.org_user_status",
|
||||||
"registrar.context_processors.add_path_to_context",
|
"registrar.context_processors.add_path_to_context",
|
||||||
"registrar.context_processors.add_has_profile_feature_flag_to_context",
|
|
||||||
"registrar.context_processors.portfolio_permissions",
|
"registrar.context_processors.portfolio_permissions",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -415,7 +417,7 @@ LANGUAGE_COOKIE_SECURE = True
|
||||||
# and to interpret datetimes entered in forms
|
# and to interpret datetimes entered in forms
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
# enable Django’s translation system
|
# enable Django's translation system
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
# enable localized formatting of numbers and dates
|
# enable localized formatting of numbers and dates
|
||||||
|
@ -450,6 +452,40 @@ PHONENUMBER_DEFAULT_REGION = "US"
|
||||||
# logger.error("Can't do this important task. Something is very wrong.")
|
# logger.error("Can't do this important task. Something is very wrong.")
|
||||||
# logger.critical("Going to crash now.")
|
# logger.critical("Going to crash now.")
|
||||||
|
|
||||||
|
|
||||||
|
class JsonFormatter(logging.Formatter):
|
||||||
|
"""Formats logs into JSON for better parsing"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(datefmt="%d/%b/%Y %H:%M:%S")
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
log_record = {
|
||||||
|
"timestamp": self.formatTime(record, self.datefmt),
|
||||||
|
"level": record.levelname,
|
||||||
|
"name": record.name,
|
||||||
|
"lineno": record.lineno,
|
||||||
|
"message": record.getMessage(),
|
||||||
|
}
|
||||||
|
return json.dumps(log_record)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonServerFormatter(ServerFormatter):
|
||||||
|
"""Formats server logs into JSON for better parsing"""
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
formatted_record = super().format(record)
|
||||||
|
log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record}
|
||||||
|
return json.dumps(log_entry)
|
||||||
|
|
||||||
|
|
||||||
|
# default to json formatted logs
|
||||||
|
server_formatter, console_formatter = "json.server", "json"
|
||||||
|
|
||||||
|
# don't use json format locally, it makes logs hard to read in console
|
||||||
|
if "localhost" in env_base_url:
|
||||||
|
server_formatter, console_formatter = "django.server", "verbose"
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
# Don't import Django's existing loggers
|
# Don't import Django's existing loggers
|
||||||
|
@ -469,6 +505,12 @@ LOGGING = {
|
||||||
"format": "[{server_time}] {message}",
|
"format": "[{server_time}] {message}",
|
||||||
"style": "{",
|
"style": "{",
|
||||||
},
|
},
|
||||||
|
"json.server": {
|
||||||
|
"()": JsonServerFormatter,
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"()": JsonFormatter,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
# define where log messages will be sent;
|
# define where log messages will be sent;
|
||||||
# each logger can have one or more handlers
|
# each logger can have one or more handlers
|
||||||
|
@ -476,12 +518,12 @@ LOGGING = {
|
||||||
"console": {
|
"console": {
|
||||||
"level": env_log_level,
|
"level": env_log_level,
|
||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "verbose",
|
"formatter": console_formatter,
|
||||||
},
|
},
|
||||||
"django.server": {
|
"django.server": {
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "django.server",
|
"formatter": server_formatter,
|
||||||
},
|
},
|
||||||
# No file logger is configured,
|
# No file logger is configured,
|
||||||
# because containerized apps
|
# because containerized apps
|
||||||
|
|
|
@ -53,7 +53,6 @@ for step, view in [
|
||||||
(Step.CURRENT_SITES, views.CurrentSites),
|
(Step.CURRENT_SITES, views.CurrentSites),
|
||||||
(Step.DOTGOV_DOMAIN, views.DotgovDomain),
|
(Step.DOTGOV_DOMAIN, views.DotgovDomain),
|
||||||
(Step.PURPOSE, views.Purpose),
|
(Step.PURPOSE, views.Purpose),
|
||||||
(Step.YOUR_CONTACT, views.YourContact),
|
|
||||||
(Step.OTHER_CONTACTS, views.OtherContacts),
|
(Step.OTHER_CONTACTS, views.OtherContacts),
|
||||||
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
||||||
(Step.REQUIREMENTS, views.Requirements),
|
(Step.REQUIREMENTS, views.Requirements),
|
||||||
|
@ -79,6 +78,11 @@ urlpatterns = [
|
||||||
views.PortfolioDomainRequestsView.as_view(),
|
views.PortfolioDomainRequestsView.as_view(),
|
||||||
name="domain-requests",
|
name="domain-requests",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"no-organization-requests/",
|
||||||
|
views.PortfolioNoDomainRequestsView.as_view(),
|
||||||
|
name="no-portfolio-requests",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"organization/",
|
"organization/",
|
||||||
views.PortfolioOrganizationView.as_view(),
|
views.PortfolioOrganizationView.as_view(),
|
||||||
|
@ -208,11 +212,6 @@ urlpatterns = [
|
||||||
views.DomainDsDataView.as_view(),
|
views.DomainDsDataView.as_view(),
|
||||||
name="domain-dns-dnssec-dsdata",
|
name="domain-dns-dnssec-dsdata",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"domain/<int:pk>/your-contact-information",
|
|
||||||
views.DomainYourContactInformationView.as_view(),
|
|
||||||
name="domain-your-contact-information",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"domain/<int:pk>/org-name-address",
|
"domain/<int:pk>/org-name-address",
|
||||||
views.DomainOrgNameAddressView.as_view(),
|
views.DomainOrgNameAddressView.as_view(),
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from waffle.decorators import flag_is_active
|
|
||||||
|
|
||||||
|
|
||||||
def language_code(request):
|
def language_code(request):
|
||||||
|
@ -54,41 +53,44 @@ def add_path_to_context(request):
|
||||||
return {"path": getattr(request, "path", None)}
|
return {"path": getattr(request, "path", None)}
|
||||||
|
|
||||||
|
|
||||||
def add_has_profile_feature_flag_to_context(request):
|
|
||||||
return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")}
|
|
||||||
|
|
||||||
|
|
||||||
def portfolio_permissions(request):
|
def portfolio_permissions(request):
|
||||||
"""Make portfolio permissions for the request user available in global context"""
|
"""Make portfolio permissions for the request user available in global context"""
|
||||||
context = {
|
portfolio_context = {
|
||||||
"has_base_portfolio_permission": False,
|
"has_base_portfolio_permission": False,
|
||||||
"has_domains_portfolio_permission": False,
|
"has_any_domains_portfolio_permission": False,
|
||||||
"has_domain_requests_portfolio_permission": False,
|
"has_any_requests_portfolio_permission": False,
|
||||||
|
"has_edit_request_portfolio_permission": False,
|
||||||
|
"has_view_suborganization_portfolio_permission": False,
|
||||||
|
"has_edit_suborganization_portfolio_permission": False,
|
||||||
"has_view_members_portfolio_permission": False,
|
"has_view_members_portfolio_permission": False,
|
||||||
"has_edit_members_portfolio_permission": False,
|
"has_edit_members_portfolio_permission": False,
|
||||||
"has_view_suborganization": False,
|
|
||||||
"has_edit_suborganization": False,
|
|
||||||
"portfolio": None,
|
"portfolio": None,
|
||||||
"has_organization_feature_flag": False,
|
"has_organization_feature_flag": False,
|
||||||
|
"has_organization_requests_flag": False,
|
||||||
|
"has_organization_members_flag": False,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
|
# Linting: line too long
|
||||||
|
view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio)
|
||||||
|
edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio)
|
||||||
if portfolio:
|
if portfolio:
|
||||||
return {
|
return {
|
||||||
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio),
|
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio),
|
||||||
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(portfolio),
|
"has_edit_request_portfolio_permission": request.user.has_edit_request_portfolio_permission(portfolio),
|
||||||
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
|
"has_view_suborganization_portfolio_permission": view_suborg,
|
||||||
portfolio
|
"has_edit_suborganization_portfolio_permission": edit_suborg,
|
||||||
),
|
"has_any_domains_portfolio_permission": request.user.has_any_domains_portfolio_permission(portfolio),
|
||||||
|
"has_any_requests_portfolio_permission": request.user.has_any_requests_portfolio_permission(portfolio),
|
||||||
"has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
|
"has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
|
||||||
"has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
|
"has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
|
||||||
"has_view_suborganization": request.user.has_view_suborganization(portfolio),
|
|
||||||
"has_edit_suborganization": request.user.has_edit_suborganization(portfolio),
|
|
||||||
"portfolio": portfolio,
|
"portfolio": portfolio,
|
||||||
"has_organization_feature_flag": True,
|
"has_organization_feature_flag": True,
|
||||||
|
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
|
||||||
|
"has_organization_members_flag": request.user.has_organization_members_flag(),
|
||||||
}
|
}
|
||||||
return context
|
return portfolio_context
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Handles cases where request.user might not exist
|
# Handles cases where request.user might not exist
|
||||||
return context
|
return portfolio_context
|
||||||
|
|
|
@ -37,7 +37,6 @@ class DomainRequestFixture:
|
||||||
# "anything_else": None,
|
# "anything_else": None,
|
||||||
# "is_policy_acknowledged": None,
|
# "is_policy_acknowledged": None,
|
||||||
# "senior_official": None,
|
# "senior_official": None,
|
||||||
# "submitter": None,
|
|
||||||
# "other_contacts": [],
|
# "other_contacts": [],
|
||||||
# "current_websites": [],
|
# "current_websites": [],
|
||||||
# "alternative_domains": [],
|
# "alternative_domains": [],
|
||||||
|
@ -123,12 +122,6 @@ class DomainRequestFixture:
|
||||||
else:
|
else:
|
||||||
da.senior_official = Contact.objects.create(**cls.fake_contact())
|
da.senior_official = Contact.objects.create(**cls.fake_contact())
|
||||||
|
|
||||||
if not da.submitter:
|
|
||||||
if "submitter" in app and app["submitter"] is not None:
|
|
||||||
da.submitter, _ = Contact.objects.get_or_create(**app["submitter"])
|
|
||||||
else:
|
|
||||||
da.submitter = Contact.objects.create(**cls.fake_contact())
|
|
||||||
|
|
||||||
if not da.requested_domain:
|
if not da.requested_domain:
|
||||||
if "requested_domain" in app and app["requested_domain"] is not None:
|
if "requested_domain" in app and app["requested_domain"] is not None:
|
||||||
da.requested_domain, _ = DraftDomain.objects.get_or_create(name=app["requested_domain"])
|
da.requested_domain, _ = DraftDomain.objects.get_or_create(name=app["requested_domain"])
|
||||||
|
|
|
@ -56,6 +56,7 @@ class UserFixture:
|
||||||
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
|
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
|
||||||
"first_name": "Katherine",
|
"first_name": "Katherine",
|
||||||
"last_name": "Osos",
|
"last_name": "Osos",
|
||||||
|
"email": "kosos@truss.works",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
|
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
|
||||||
|
@ -171,7 +172,7 @@ class UserFixture:
|
||||||
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
||||||
"first_name": "Katherine-Analyst",
|
"first_name": "Katherine-Analyst",
|
||||||
"last_name": "Osos-Analyst",
|
"last_name": "Osos-Analyst",
|
||||||
"email": "kosos@truss.works",
|
"email": "kosos+1@truss.works",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
|
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
|
||||||
|
|
|
@ -386,64 +386,6 @@ class PurposeForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class YourContactForm(RegistrarForm):
|
|
||||||
JOIN = "submitter"
|
|
||||||
|
|
||||||
def to_database(self, obj):
|
|
||||||
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_domain_requests"):
|
|
||||||
# if contact exists in the database and is not joined to other entities
|
|
||||||
super().to_database(contact)
|
|
||||||
else:
|
|
||||||
# no contact exists OR contact exists which is joined also to other entities;
|
|
||||||
# in either case, create a new contact and update it
|
|
||||||
contact = Contact()
|
|
||||||
super().to_database(contact)
|
|
||||||
obj.submitter = contact
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_database(cls, obj):
|
|
||||||
contact = getattr(obj, "submitter", None)
|
|
||||||
return super().from_database(contact)
|
|
||||||
|
|
||||||
first_name = forms.CharField(
|
|
||||||
label="First name / given name",
|
|
||||||
error_messages={"required": "Enter your first name / given name."},
|
|
||||||
)
|
|
||||||
middle_name = forms.CharField(
|
|
||||||
required=False,
|
|
||||||
label="Middle name (optional)",
|
|
||||||
)
|
|
||||||
last_name = forms.CharField(
|
|
||||||
label="Last name / family name",
|
|
||||||
error_messages={"required": "Enter your last name / family name."},
|
|
||||||
)
|
|
||||||
title = forms.CharField(
|
|
||||||
label="Title or role in your organization",
|
|
||||||
error_messages={
|
|
||||||
"required": ("Enter your title or role in your organization (e.g., Chief Information Officer).")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
email = forms.EmailField(
|
|
||||||
label="Email",
|
|
||||||
max_length=None,
|
|
||||||
error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")},
|
|
||||||
validators=[
|
|
||||||
MaxLengthValidator(
|
|
||||||
320,
|
|
||||||
message="Response must be less than 320 characters.",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
phone = PhoneNumberField(
|
|
||||||
label="Phone",
|
|
||||||
error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OtherContactsYesNoForm(BaseYesNoForm):
|
class OtherContactsYesNoForm(BaseYesNoForm):
|
||||||
"""The yes/no field for the OtherContacts form."""
|
"""The yes/no field for the OtherContacts form."""
|
||||||
|
|
||||||
|
|
255
src/registrar/management/commands/create_federal_portfolio.py
Normal file
255
src/registrar/management/commands/create_federal_portfolio.py
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
"""Loads files from /tmp into our sandboxes"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from django.core.management import BaseCommand, CommandError
|
||||||
|
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
|
||||||
|
from registrar.models import DomainInformation, DomainRequest, FederalAgency, Suborganization, Portfolio, User
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Creates a federal portfolio given a FederalAgency name"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""Add three arguments:
|
||||||
|
1. agency_name => the value of FederalAgency.agency
|
||||||
|
2. --parse_requests => if true, adds the given portfolio to each related DomainRequest
|
||||||
|
3. --parse_domains => if true, adds the given portfolio to each related DomainInformation
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
"agency_name",
|
||||||
|
help="The name of the FederalAgency to add",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--parse_requests",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
help="Adds portfolio to DomainRequests",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--parse_domains",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
help="Adds portfolio to DomainInformation",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--both",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
help="Adds portfolio to both requests and domains",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, agency_name, **options):
|
||||||
|
parse_requests = options.get("parse_requests")
|
||||||
|
parse_domains = options.get("parse_domains")
|
||||||
|
both = options.get("both")
|
||||||
|
|
||||||
|
if not both:
|
||||||
|
if not parse_requests and not parse_domains:
|
||||||
|
raise CommandError("You must specify at least one of --parse_requests or --parse_domains.")
|
||||||
|
else:
|
||||||
|
if parse_requests or parse_domains:
|
||||||
|
raise CommandError("You cannot pass --parse_requests or --parse_domains when passing --both.")
|
||||||
|
|
||||||
|
federal_agency = FederalAgency.objects.filter(agency__iexact=agency_name).first()
|
||||||
|
if not federal_agency:
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot find the federal agency '{agency_name}' in our database. "
|
||||||
|
"The value you enter for `agency_name` must be "
|
||||||
|
"prepopulated in the FederalAgency table before proceeding."
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio = self.create_or_modify_portfolio(federal_agency)
|
||||||
|
self.create_suborganizations(portfolio, federal_agency)
|
||||||
|
|
||||||
|
if parse_requests or both:
|
||||||
|
self.handle_portfolio_requests(portfolio, federal_agency)
|
||||||
|
|
||||||
|
if parse_domains or both:
|
||||||
|
self.handle_portfolio_domains(portfolio, federal_agency)
|
||||||
|
|
||||||
|
def create_or_modify_portfolio(self, federal_agency):
|
||||||
|
"""Creates or modifies a portfolio record based on a federal agency."""
|
||||||
|
portfolio_args = {
|
||||||
|
"federal_agency": federal_agency,
|
||||||
|
"organization_name": federal_agency.agency,
|
||||||
|
"organization_type": DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
"creator": User.get_default_user(),
|
||||||
|
"notes": "Auto-generated record",
|
||||||
|
}
|
||||||
|
|
||||||
|
if federal_agency.so_federal_agency.exists():
|
||||||
|
portfolio_args["senior_official"] = federal_agency.so_federal_agency.first()
|
||||||
|
|
||||||
|
portfolio, created = Portfolio.objects.get_or_create(
|
||||||
|
organization_name=portfolio_args.get("organization_name"), defaults=portfolio_args
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
message = f"Created portfolio '{portfolio}'"
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||||
|
|
||||||
|
if portfolio_args.get("senior_official"):
|
||||||
|
message = f"Added senior official '{portfolio_args['senior_official']}'"
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||||
|
else:
|
||||||
|
message = (
|
||||||
|
f"No senior official added to portfolio '{portfolio}'. "
|
||||||
|
"None was returned for the reverse relation `FederalAgency.so_federal_agency.first()`"
|
||||||
|
)
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
|
else:
|
||||||
|
proceed = TerminalHelper.prompt_for_execution(
|
||||||
|
system_exit_on_terminate=False,
|
||||||
|
prompt_message=f"""
|
||||||
|
The given portfolio '{federal_agency.agency}' already exists in our DB.
|
||||||
|
If you cancel, the rest of the script will still execute but this record will not update.
|
||||||
|
""",
|
||||||
|
prompt_title="Do you wish to modify this record?",
|
||||||
|
)
|
||||||
|
if proceed:
|
||||||
|
|
||||||
|
# Don't override the creator and notes fields
|
||||||
|
if portfolio.creator:
|
||||||
|
portfolio_args.pop("creator")
|
||||||
|
|
||||||
|
if portfolio.notes:
|
||||||
|
portfolio_args.pop("notes")
|
||||||
|
|
||||||
|
# Update everything else
|
||||||
|
for key, value in portfolio_args.items():
|
||||||
|
setattr(portfolio, key, value)
|
||||||
|
|
||||||
|
portfolio.save()
|
||||||
|
message = f"Modified portfolio '{portfolio}'"
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
|
|
||||||
|
if portfolio_args.get("senior_official"):
|
||||||
|
message = f"Added/modified senior official '{portfolio_args['senior_official']}'"
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
|
|
||||||
|
return portfolio
|
||||||
|
|
||||||
|
def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||||
|
"""Create Suborganizations tied to the given portfolio based on DomainInformation objects"""
|
||||||
|
valid_agencies = DomainInformation.objects.filter(
|
||||||
|
federal_agency=federal_agency, organization_name__isnull=False
|
||||||
|
)
|
||||||
|
org_names = set(valid_agencies.values_list("organization_name", flat=True))
|
||||||
|
|
||||||
|
if not org_names:
|
||||||
|
message = (
|
||||||
|
"Could not add any suborganizations."
|
||||||
|
f"\nNo suborganizations were found for '{federal_agency}' when filtering on this name, "
|
||||||
|
"and excluding null organization_name records."
|
||||||
|
)
|
||||||
|
TerminalHelper.colorful_logger(logger.warning, TerminalColors.FAIL, message)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if we need to update any existing suborgs first. This step is optional.
|
||||||
|
existing_suborgs = Suborganization.objects.filter(name__in=org_names)
|
||||||
|
if existing_suborgs.exists():
|
||||||
|
self._update_existing_suborganizations(portfolio, existing_suborgs)
|
||||||
|
|
||||||
|
# Create new suborgs, as long as they don't exist in the db already
|
||||||
|
new_suborgs = []
|
||||||
|
for name in org_names - set(existing_suborgs.values_list("name", flat=True)):
|
||||||
|
# Stored in variables due to linter wanting type information here.
|
||||||
|
portfolio_name: str = portfolio.organization_name if portfolio.organization_name is not None else ""
|
||||||
|
if name is not None and name.lower() == portfolio_name.lower():
|
||||||
|
# You can use this to populate location information, when this occurs.
|
||||||
|
# However, this isn't needed for now so we can skip it.
|
||||||
|
message = (
|
||||||
|
f"Skipping suborganization create on record '{name}'. "
|
||||||
|
"The federal agency name is the same as the portfolio name."
|
||||||
|
)
|
||||||
|
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message)
|
||||||
|
else:
|
||||||
|
new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) # type: ignore
|
||||||
|
|
||||||
|
if new_suborgs:
|
||||||
|
Suborganization.objects.bulk_create(new_suborgs)
|
||||||
|
TerminalHelper.colorful_logger(
|
||||||
|
logger.info, TerminalColors.OKGREEN, f"Added {len(new_suborgs)} suborganizations"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, "No suborganizations added")
|
||||||
|
|
||||||
|
def _update_existing_suborganizations(self, portfolio, orgs_to_update):
|
||||||
|
"""
|
||||||
|
Update existing suborganizations with new portfolio.
|
||||||
|
Prompts for user confirmation before proceeding.
|
||||||
|
"""
|
||||||
|
proceed = TerminalHelper.prompt_for_execution(
|
||||||
|
system_exit_on_terminate=False,
|
||||||
|
prompt_message=f"""Some suborganizations already exist in our DB.
|
||||||
|
If you cancel, the rest of the script will still execute but these records will not update.
|
||||||
|
|
||||||
|
==Proposed Changes==
|
||||||
|
The following suborgs will be updated: {[org.name for org in orgs_to_update]}
|
||||||
|
""",
|
||||||
|
prompt_title="Do you wish to modify existing suborganizations?",
|
||||||
|
)
|
||||||
|
if proceed:
|
||||||
|
for org in orgs_to_update:
|
||||||
|
org.portfolio = portfolio
|
||||||
|
|
||||||
|
Suborganization.objects.bulk_update(orgs_to_update, ["portfolio"])
|
||||||
|
message = f"Updated {len(orgs_to_update)} suborganizations."
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
|
|
||||||
|
def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||||
|
"""
|
||||||
|
Associate portfolio with domain requests for a federal agency.
|
||||||
|
Updates all relevant domain request records.
|
||||||
|
"""
|
||||||
|
invalid_states = [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||||
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
]
|
||||||
|
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency).exclude(status__in=invalid_states)
|
||||||
|
if not domain_requests.exists():
|
||||||
|
message = f"""
|
||||||
|
Portfolios not added to domain requests: no valid records found.
|
||||||
|
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
|
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
||||||
|
"""
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get all suborg information and store it in a dict to avoid doing a db call
|
||||||
|
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
|
||||||
|
for domain_request in domain_requests:
|
||||||
|
domain_request.portfolio = portfolio
|
||||||
|
if domain_request.organization_name in suborgs:
|
||||||
|
domain_request.sub_organization = suborgs.get(domain_request.organization_name)
|
||||||
|
|
||||||
|
DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"])
|
||||||
|
message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests."
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||||
|
|
||||||
|
def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||||
|
"""
|
||||||
|
Associate portfolio with domains for a federal agency.
|
||||||
|
Updates all relevant domain information records.
|
||||||
|
"""
|
||||||
|
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency)
|
||||||
|
if not domain_infos.exists():
|
||||||
|
message = f"""
|
||||||
|
Portfolios not added to domains: no valid records found.
|
||||||
|
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
|
"""
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get all suborg information and store it in a dict to avoid doing a db call
|
||||||
|
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
|
||||||
|
for domain_info in domain_infos:
|
||||||
|
domain_info.portfolio = portfolio
|
||||||
|
if domain_info.organization_name in suborgs:
|
||||||
|
domain_info.sub_organization = suborgs.get(domain_info.organization_name)
|
||||||
|
|
||||||
|
DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"])
|
||||||
|
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains"
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
|
@ -423,7 +423,7 @@ class Command(BaseCommand):
|
||||||
valid_fed_type = fed_type in fed_choices
|
valid_fed_type = fed_type in fed_choices
|
||||||
valid_fed_agency = fed_agency in agency_choices
|
valid_fed_agency = fed_agency in agency_choices
|
||||||
|
|
||||||
default_creator, _ = User.objects.get_or_create(username="System")
|
default_creator = User.get_default_user()
|
||||||
|
|
||||||
new_domain_info_data = {
|
new_domain_info_data = {
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.db.models.manager import BaseManager
|
|
||||||
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||||
from registrar.models import Domain, TransitionDomain
|
from registrar.models import Domain, TransitionDomain
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-09-09 14:48
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="portfolioinvitation",
|
||||||
|
name="portfolio_additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "View all domains and domain reports"),
|
||||||
|
("view_managed_domains", "View managed domains"),
|
||||||
|
("view_members", "View members"),
|
||||||
|
("edit_members", "Create and edit members"),
|
||||||
|
("view_all_requests", "View all requests"),
|
||||||
|
("edit_requests", "Create and edit requests"),
|
||||||
|
("view_portfolio", "View organization"),
|
||||||
|
("edit_portfolio", "Edit organization"),
|
||||||
|
("view_suborganization", "View suborganization"),
|
||||||
|
("edit_suborganization", "Edit suborganization"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userportfoliopermission",
|
||||||
|
name="additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "View all domains and domain reports"),
|
||||||
|
("view_managed_domains", "View managed domains"),
|
||||||
|
("view_members", "View members"),
|
||||||
|
("edit_members", "Create and edit members"),
|
||||||
|
("view_all_requests", "View all requests"),
|
||||||
|
("edit_requests", "Create and edit requests"),
|
||||||
|
("view_portfolio", "View organization"),
|
||||||
|
("edit_portfolio", "Edit organization"),
|
||||||
|
("view_suborganization", "View suborganization"),
|
||||||
|
("edit_suborganization", "Edit suborganization"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-08-29 23:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0124_alter_portfolioinvitation_portfolio_additional_permissions_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="submitter",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text='Person listed under "your contact information" in the request form',
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="submitted_domain_requests_information",
|
||||||
|
to="registrar.contact",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="submitter",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text='Person listed under "your contact information" in the request form; will receive email updates',
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="submitted_domain_requests",
|
||||||
|
to="registrar.contact",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,27 @@
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import Q
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
# Deletes Contact objects associated with a submitter which we are deprecating
|
||||||
|
def cascade_delete_submitter_contacts(apps, schema_editor) -> Any:
|
||||||
|
contacts_model = apps.get_model("registrar", "Contact")
|
||||||
|
submitter_contacts = contacts_model.objects.filter(
|
||||||
|
Q(submitted_domain_requests__isnull=False) | Q(submitted_domain_requests_information__isnull=False)
|
||||||
|
).filter(
|
||||||
|
information_senior_official__isnull=True,
|
||||||
|
senior_official__isnull=True,
|
||||||
|
contact_domain_requests_information__isnull=True,
|
||||||
|
contact_domain_requests__isnull=True,
|
||||||
|
)
|
||||||
|
submitter_contacts.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0125_alter_domaininformation_submitter_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(cascade_delete_submitter_contacts, reverse_code=migrations.RunPython.noop, atomic=True),
|
||||||
|
]
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-08-29 24:13
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0126_delete_cascade_submitter_contacts"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="submitter",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="submitter",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="creator",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Person who submitted the domain request. Will receive email updates.",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="domain_requests_created",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,229 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-09-11 21:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0127_remove_domaininformation_submitter_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="state_territory",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("AL", "Alabama (AL)"),
|
||||||
|
("AK", "Alaska (AK)"),
|
||||||
|
("AS", "American Samoa (AS)"),
|
||||||
|
("AZ", "Arizona (AZ)"),
|
||||||
|
("AR", "Arkansas (AR)"),
|
||||||
|
("CA", "California (CA)"),
|
||||||
|
("CO", "Colorado (CO)"),
|
||||||
|
("CT", "Connecticut (CT)"),
|
||||||
|
("DE", "Delaware (DE)"),
|
||||||
|
("DC", "District of Columbia (DC)"),
|
||||||
|
("FL", "Florida (FL)"),
|
||||||
|
("GA", "Georgia (GA)"),
|
||||||
|
("GU", "Guam (GU)"),
|
||||||
|
("HI", "Hawaii (HI)"),
|
||||||
|
("ID", "Idaho (ID)"),
|
||||||
|
("IL", "Illinois (IL)"),
|
||||||
|
("IN", "Indiana (IN)"),
|
||||||
|
("IA", "Iowa (IA)"),
|
||||||
|
("KS", "Kansas (KS)"),
|
||||||
|
("KY", "Kentucky (KY)"),
|
||||||
|
("LA", "Louisiana (LA)"),
|
||||||
|
("ME", "Maine (ME)"),
|
||||||
|
("MD", "Maryland (MD)"),
|
||||||
|
("MA", "Massachusetts (MA)"),
|
||||||
|
("MI", "Michigan (MI)"),
|
||||||
|
("MN", "Minnesota (MN)"),
|
||||||
|
("MS", "Mississippi (MS)"),
|
||||||
|
("MO", "Missouri (MO)"),
|
||||||
|
("MT", "Montana (MT)"),
|
||||||
|
("NE", "Nebraska (NE)"),
|
||||||
|
("NV", "Nevada (NV)"),
|
||||||
|
("NH", "New Hampshire (NH)"),
|
||||||
|
("NJ", "New Jersey (NJ)"),
|
||||||
|
("NM", "New Mexico (NM)"),
|
||||||
|
("NY", "New York (NY)"),
|
||||||
|
("NC", "North Carolina (NC)"),
|
||||||
|
("ND", "North Dakota (ND)"),
|
||||||
|
("MP", "Northern Mariana Islands (MP)"),
|
||||||
|
("OH", "Ohio (OH)"),
|
||||||
|
("OK", "Oklahoma (OK)"),
|
||||||
|
("OR", "Oregon (OR)"),
|
||||||
|
("PA", "Pennsylvania (PA)"),
|
||||||
|
("PR", "Puerto Rico (PR)"),
|
||||||
|
("RI", "Rhode Island (RI)"),
|
||||||
|
("SC", "South Carolina (SC)"),
|
||||||
|
("SD", "South Dakota (SD)"),
|
||||||
|
("TN", "Tennessee (TN)"),
|
||||||
|
("TX", "Texas (TX)"),
|
||||||
|
("UM", "United States Minor Outlying Islands (UM)"),
|
||||||
|
("UT", "Utah (UT)"),
|
||||||
|
("VT", "Vermont (VT)"),
|
||||||
|
("VI", "Virgin Islands (VI)"),
|
||||||
|
("VA", "Virginia (VA)"),
|
||||||
|
("WA", "Washington (WA)"),
|
||||||
|
("WV", "West Virginia (WV)"),
|
||||||
|
("WI", "Wisconsin (WI)"),
|
||||||
|
("WY", "Wyoming (WY)"),
|
||||||
|
("AA", "Armed Forces Americas (AA)"),
|
||||||
|
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||||
|
("AP", "Armed Forces Pacific (AP)"),
|
||||||
|
],
|
||||||
|
max_length=2,
|
||||||
|
null=True,
|
||||||
|
verbose_name="state, territory, or military post",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="state_territory",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("AL", "Alabama (AL)"),
|
||||||
|
("AK", "Alaska (AK)"),
|
||||||
|
("AS", "American Samoa (AS)"),
|
||||||
|
("AZ", "Arizona (AZ)"),
|
||||||
|
("AR", "Arkansas (AR)"),
|
||||||
|
("CA", "California (CA)"),
|
||||||
|
("CO", "Colorado (CO)"),
|
||||||
|
("CT", "Connecticut (CT)"),
|
||||||
|
("DE", "Delaware (DE)"),
|
||||||
|
("DC", "District of Columbia (DC)"),
|
||||||
|
("FL", "Florida (FL)"),
|
||||||
|
("GA", "Georgia (GA)"),
|
||||||
|
("GU", "Guam (GU)"),
|
||||||
|
("HI", "Hawaii (HI)"),
|
||||||
|
("ID", "Idaho (ID)"),
|
||||||
|
("IL", "Illinois (IL)"),
|
||||||
|
("IN", "Indiana (IN)"),
|
||||||
|
("IA", "Iowa (IA)"),
|
||||||
|
("KS", "Kansas (KS)"),
|
||||||
|
("KY", "Kentucky (KY)"),
|
||||||
|
("LA", "Louisiana (LA)"),
|
||||||
|
("ME", "Maine (ME)"),
|
||||||
|
("MD", "Maryland (MD)"),
|
||||||
|
("MA", "Massachusetts (MA)"),
|
||||||
|
("MI", "Michigan (MI)"),
|
||||||
|
("MN", "Minnesota (MN)"),
|
||||||
|
("MS", "Mississippi (MS)"),
|
||||||
|
("MO", "Missouri (MO)"),
|
||||||
|
("MT", "Montana (MT)"),
|
||||||
|
("NE", "Nebraska (NE)"),
|
||||||
|
("NV", "Nevada (NV)"),
|
||||||
|
("NH", "New Hampshire (NH)"),
|
||||||
|
("NJ", "New Jersey (NJ)"),
|
||||||
|
("NM", "New Mexico (NM)"),
|
||||||
|
("NY", "New York (NY)"),
|
||||||
|
("NC", "North Carolina (NC)"),
|
||||||
|
("ND", "North Dakota (ND)"),
|
||||||
|
("MP", "Northern Mariana Islands (MP)"),
|
||||||
|
("OH", "Ohio (OH)"),
|
||||||
|
("OK", "Oklahoma (OK)"),
|
||||||
|
("OR", "Oregon (OR)"),
|
||||||
|
("PA", "Pennsylvania (PA)"),
|
||||||
|
("PR", "Puerto Rico (PR)"),
|
||||||
|
("RI", "Rhode Island (RI)"),
|
||||||
|
("SC", "South Carolina (SC)"),
|
||||||
|
("SD", "South Dakota (SD)"),
|
||||||
|
("TN", "Tennessee (TN)"),
|
||||||
|
("TX", "Texas (TX)"),
|
||||||
|
("UM", "United States Minor Outlying Islands (UM)"),
|
||||||
|
("UT", "Utah (UT)"),
|
||||||
|
("VT", "Vermont (VT)"),
|
||||||
|
("VI", "Virgin Islands (VI)"),
|
||||||
|
("VA", "Virginia (VA)"),
|
||||||
|
("WA", "Washington (WA)"),
|
||||||
|
("WV", "West Virginia (WV)"),
|
||||||
|
("WI", "Wisconsin (WI)"),
|
||||||
|
("WY", "Wyoming (WY)"),
|
||||||
|
("AA", "Armed Forces Americas (AA)"),
|
||||||
|
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||||
|
("AP", "Armed Forces Pacific (AP)"),
|
||||||
|
],
|
||||||
|
max_length=2,
|
||||||
|
null=True,
|
||||||
|
verbose_name="state, territory, or military post",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="portfolio",
|
||||||
|
name="state_territory",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("AL", "Alabama (AL)"),
|
||||||
|
("AK", "Alaska (AK)"),
|
||||||
|
("AS", "American Samoa (AS)"),
|
||||||
|
("AZ", "Arizona (AZ)"),
|
||||||
|
("AR", "Arkansas (AR)"),
|
||||||
|
("CA", "California (CA)"),
|
||||||
|
("CO", "Colorado (CO)"),
|
||||||
|
("CT", "Connecticut (CT)"),
|
||||||
|
("DE", "Delaware (DE)"),
|
||||||
|
("DC", "District of Columbia (DC)"),
|
||||||
|
("FL", "Florida (FL)"),
|
||||||
|
("GA", "Georgia (GA)"),
|
||||||
|
("GU", "Guam (GU)"),
|
||||||
|
("HI", "Hawaii (HI)"),
|
||||||
|
("ID", "Idaho (ID)"),
|
||||||
|
("IL", "Illinois (IL)"),
|
||||||
|
("IN", "Indiana (IN)"),
|
||||||
|
("IA", "Iowa (IA)"),
|
||||||
|
("KS", "Kansas (KS)"),
|
||||||
|
("KY", "Kentucky (KY)"),
|
||||||
|
("LA", "Louisiana (LA)"),
|
||||||
|
("ME", "Maine (ME)"),
|
||||||
|
("MD", "Maryland (MD)"),
|
||||||
|
("MA", "Massachusetts (MA)"),
|
||||||
|
("MI", "Michigan (MI)"),
|
||||||
|
("MN", "Minnesota (MN)"),
|
||||||
|
("MS", "Mississippi (MS)"),
|
||||||
|
("MO", "Missouri (MO)"),
|
||||||
|
("MT", "Montana (MT)"),
|
||||||
|
("NE", "Nebraska (NE)"),
|
||||||
|
("NV", "Nevada (NV)"),
|
||||||
|
("NH", "New Hampshire (NH)"),
|
||||||
|
("NJ", "New Jersey (NJ)"),
|
||||||
|
("NM", "New Mexico (NM)"),
|
||||||
|
("NY", "New York (NY)"),
|
||||||
|
("NC", "North Carolina (NC)"),
|
||||||
|
("ND", "North Dakota (ND)"),
|
||||||
|
("MP", "Northern Mariana Islands (MP)"),
|
||||||
|
("OH", "Ohio (OH)"),
|
||||||
|
("OK", "Oklahoma (OK)"),
|
||||||
|
("OR", "Oregon (OR)"),
|
||||||
|
("PA", "Pennsylvania (PA)"),
|
||||||
|
("PR", "Puerto Rico (PR)"),
|
||||||
|
("RI", "Rhode Island (RI)"),
|
||||||
|
("SC", "South Carolina (SC)"),
|
||||||
|
("SD", "South Dakota (SD)"),
|
||||||
|
("TN", "Tennessee (TN)"),
|
||||||
|
("TX", "Texas (TX)"),
|
||||||
|
("UM", "United States Minor Outlying Islands (UM)"),
|
||||||
|
("UT", "Utah (UT)"),
|
||||||
|
("VT", "Vermont (VT)"),
|
||||||
|
("VI", "Virgin Islands (VI)"),
|
||||||
|
("VA", "Virginia (VA)"),
|
||||||
|
("WA", "Washington (WA)"),
|
||||||
|
("WV", "West Virginia (WV)"),
|
||||||
|
("WI", "Wisconsin (WI)"),
|
||||||
|
("WY", "Wyoming (WY)"),
|
||||||
|
("AA", "Armed Forces Americas (AA)"),
|
||||||
|
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||||
|
("AP", "Armed Forces Pacific (AP)"),
|
||||||
|
],
|
||||||
|
max_length=2,
|
||||||
|
null=True,
|
||||||
|
verbose_name="state, territory, or military post",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -48,8 +48,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# This is the domain request user who created this domain request. The contact
|
# This is the domain request user who created this domain request.
|
||||||
# information that they gave is in the `submitter` field
|
|
||||||
creator = models.ForeignKey(
|
creator = models.ForeignKey(
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
@ -160,7 +159,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
choices=StateTerritoryChoices.choices,
|
choices=StateTerritoryChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="state / territory",
|
verbose_name="state, territory, or military post",
|
||||||
)
|
)
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
|
@ -197,17 +196,6 @@ class DomainInformation(TimeStampedModel):
|
||||||
related_name="domain_info",
|
related_name="domain_info",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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_domain_requests_information",
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
help_text='Person listed under "your contact information" in the request form',
|
|
||||||
)
|
|
||||||
|
|
||||||
purpose = models.TextField(
|
purpose = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|
|
@ -6,12 +6,12 @@ from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition # type: ignore
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from waffle import flag_is_active
|
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
|
from auditlog.models import LogEntry
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
|
@ -339,13 +339,12 @@ class DomainRequest(TimeStampedModel):
|
||||||
help_text="The suborganization that this domain request is included under",
|
help_text="The suborganization that this domain request is included under",
|
||||||
)
|
)
|
||||||
|
|
||||||
# This is the domain request user who created this domain request. The contact
|
# This is the domain request user who created this domain request.
|
||||||
# information that they gave is in the `submitter` field
|
|
||||||
creator = models.ForeignKey(
|
creator = models.ForeignKey(
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name="domain_requests_created",
|
related_name="domain_requests_created",
|
||||||
help_text="Person who submitted the domain request; will not receive email updates",
|
help_text="Person who submitted the domain request. Will receive email updates.",
|
||||||
)
|
)
|
||||||
|
|
||||||
investigator = models.ForeignKey(
|
investigator = models.ForeignKey(
|
||||||
|
@ -424,7 +423,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
choices=StateTerritoryChoices.choices,
|
choices=StateTerritoryChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="state / territory",
|
verbose_name="state, territory, or military post",
|
||||||
)
|
)
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
|
@ -483,17 +482,6 @@ class DomainRequest(TimeStampedModel):
|
||||||
help_text="Other domain names the creator provided for consideration",
|
help_text="Other domain names the creator provided for consideration",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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_domain_requests",
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
help_text='Person listed under "your contact information" in the request form; will receive email updates',
|
|
||||||
)
|
|
||||||
|
|
||||||
purpose = models.TextField(
|
purpose = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -589,11 +577,25 @@ class DomainRequest(TimeStampedModel):
|
||||||
verbose_name="last updated on",
|
verbose_name="last updated on",
|
||||||
help_text="Date of the last status update",
|
help_text="Date of the last status update",
|
||||||
)
|
)
|
||||||
|
|
||||||
notes = models.TextField(
|
notes = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_first_status_set_date(self, status):
|
||||||
|
"""Returns the date when the domain request was first set to the given status."""
|
||||||
|
log_entry = (
|
||||||
|
LogEntry.objects.filter(content_type__model="domainrequest", object_pk=self.pk, changes__status__1=status)
|
||||||
|
.order_by("-timestamp")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return log_entry.timestamp.date() if log_entry else None
|
||||||
|
|
||||||
|
def get_first_status_started_date(self):
|
||||||
|
"""Returns the date when the domain request was put into the status "started" for the first time"""
|
||||||
|
return self.get_first_status_set_date(DomainRequest.DomainRequestStatus.STARTED)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_statuses_that_send_emails(cls):
|
def get_statuses_that_send_emails(cls):
|
||||||
"""Returns a list of statuses that send an email to the user"""
|
"""Returns a list of statuses that send an email to the user"""
|
||||||
|
@ -741,9 +743,6 @@ class DomainRequest(TimeStampedModel):
|
||||||
contact information. If there is not creator information, then do
|
contact information. If there is not creator information, then do
|
||||||
nothing.
|
nothing.
|
||||||
|
|
||||||
If the waffle flag "profile_feature" is active, then this email will be sent to the
|
|
||||||
domain request creator rather than the submitter
|
|
||||||
|
|
||||||
Optional args:
|
Optional args:
|
||||||
bcc_address: str -> the address to bcc to
|
bcc_address: str -> the address to bcc to
|
||||||
|
|
||||||
|
@ -758,7 +757,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
custom_email_content: str -> Renders an email with the content of this string as its body text.
|
custom_email_content: str -> Renders an email with the content of this string as its body text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter
|
recipient = self.creator
|
||||||
if recipient is None or recipient.email is None:
|
if recipient is None or recipient.email is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Cannot send {new_status} email, no creator email address for domain request with pk: {self.pk}."
|
f"Cannot send {new_status} email, no creator email address for domain request with pk: {self.pk}."
|
||||||
|
@ -1154,6 +1153,11 @@ class DomainRequest(TimeStampedModel):
|
||||||
data[field.name] = field.value_from_object(self)
|
data[field.name] = field.value_from_object(self)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def get_formatted_cisa_rep_name(self):
|
||||||
|
"""Returns the cisa representatives name in Western order."""
|
||||||
|
names = [n for n in [self.cisa_representative_first_name, self.cisa_representative_last_name] if n]
|
||||||
|
return " ".join(names) if names else "Unknown"
|
||||||
|
|
||||||
def _is_federal_complete(self):
|
def _is_federal_complete(self):
|
||||||
# Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None
|
# Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None
|
||||||
return not (self.federal_type is None or self.federal_agency is None)
|
return not (self.federal_type is None or self.federal_agency is None)
|
||||||
|
@ -1182,6 +1186,10 @@ class DomainRequest(TimeStampedModel):
|
||||||
# Special District -> "Election office" and "About your organization" page can't be empty
|
# Special District -> "Election office" and "About your organization" page can't be empty
|
||||||
return self.is_election_board is not None and self.about_your_organization is not None
|
return self.is_election_board is not None and self.about_your_organization is not None
|
||||||
|
|
||||||
|
# Do we still want to test this after creator is autogenerated? Currently it went back to being selectable
|
||||||
|
def _is_creator_complete(self):
|
||||||
|
return self.creator is not None
|
||||||
|
|
||||||
def _is_organization_name_and_address_complete(self):
|
def _is_organization_name_and_address_complete(self):
|
||||||
return not (
|
return not (
|
||||||
self.organization_name is None
|
self.organization_name is None
|
||||||
|
@ -1200,9 +1208,6 @@ class DomainRequest(TimeStampedModel):
|
||||||
def _is_purpose_complete(self):
|
def _is_purpose_complete(self):
|
||||||
return self.purpose is not None
|
return self.purpose is not None
|
||||||
|
|
||||||
def _is_submitter_complete(self):
|
|
||||||
return self.submitter is not None
|
|
||||||
|
|
||||||
def _has_other_contacts_and_filled(self):
|
def _has_other_contacts_and_filled(self):
|
||||||
# Other Contacts Radio button is Yes and if all required fields are filled
|
# Other Contacts Radio button is Yes and if all required fields are filled
|
||||||
return (
|
return (
|
||||||
|
@ -1251,14 +1256,12 @@ class DomainRequest(TimeStampedModel):
|
||||||
return self.is_policy_acknowledged is not None
|
return self.is_policy_acknowledged is not None
|
||||||
|
|
||||||
def _is_general_form_complete(self, request):
|
def _is_general_form_complete(self, request):
|
||||||
has_profile_feature_flag = flag_is_active(request, "profile_feature")
|
|
||||||
return (
|
return (
|
||||||
self._is_organization_name_and_address_complete()
|
self._is_creator_complete()
|
||||||
|
and self._is_organization_name_and_address_complete()
|
||||||
and self._is_senior_official_complete()
|
and self._is_senior_official_complete()
|
||||||
and self._is_requested_domain_complete()
|
and self._is_requested_domain_complete()
|
||||||
and self._is_purpose_complete()
|
and self._is_purpose_complete()
|
||||||
# NOTE: This flag leaves submitter as empty (request wont submit) hence set to True
|
|
||||||
and (self._is_submitter_complete() if not has_profile_feature_flag else True)
|
|
||||||
and self._is_other_contacts_complete()
|
and self._is_other_contacts_complete()
|
||||||
and self._is_additional_details_complete()
|
and self._is_additional_details_complete()
|
||||||
and self._is_policy_acknowledgement_complete()
|
and self._is_policy_acknowledgement_complete()
|
||||||
|
|
|
@ -89,7 +89,7 @@ class Portfolio(TimeStampedModel):
|
||||||
choices=StateTerritoryChoices.choices,
|
choices=StateTerritoryChoices.choices,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="state / territory",
|
verbose_name="state, territory, or military post",
|
||||||
)
|
)
|
||||||
|
|
||||||
zipcode = models.CharField(
|
zipcode = models.CharField(
|
||||||
|
|
|
@ -131,6 +131,12 @@ class User(AbstractUser):
|
||||||
else:
|
else:
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default_user(cls):
|
||||||
|
"""Returns the default "system" user"""
|
||||||
|
default_creator, _ = User.objects.get_or_create(username="System")
|
||||||
|
return default_creator
|
||||||
|
|
||||||
def restrict_user(self):
|
def restrict_user(self):
|
||||||
self.status = self.RESTRICTED
|
self.status = self.RESTRICTED
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -192,31 +198,25 @@ class User(AbstractUser):
|
||||||
def has_edit_org_portfolio_permission(self, portfolio):
|
def has_edit_org_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
||||||
|
|
||||||
def has_domains_portfolio_permission(self, portfolio):
|
def has_any_domains_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(
|
return self._has_portfolio_permission(
|
||||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
|
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
|
||||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||||
|
|
||||||
def has_domain_requests_portfolio_permission(self, portfolio):
|
def has_organization_requests_flag(self):
|
||||||
# BEGIN
|
|
||||||
# Note code below is to add organization_request feature
|
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.user = self
|
request.user = self
|
||||||
has_organization_requests_flag = flag_is_active(request, "organization_requests")
|
return flag_is_active(request, "organization_requests")
|
||||||
if not has_organization_requests_flag:
|
|
||||||
return False
|
def has_organization_members_flag(self):
|
||||||
# END
|
request = HttpRequest()
|
||||||
return self._has_portfolio_permission(
|
request.user = self
|
||||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
return flag_is_active(request, "organization_members")
|
||||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
|
|
||||||
|
|
||||||
def has_view_members_portfolio_permission(self, portfolio):
|
def has_view_members_portfolio_permission(self, portfolio):
|
||||||
# BEGIN
|
# BEGIN
|
||||||
# Note code below is to add organization_request feature
|
# Note code below is to add organization_request feature
|
||||||
request = HttpRequest()
|
if not self.has_organization_members_flag():
|
||||||
request.user = self
|
|
||||||
has_organization_members_flag = flag_is_active(request, "organization_members")
|
|
||||||
if not has_organization_members_flag:
|
|
||||||
return False
|
return False
|
||||||
# END
|
# END
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
||||||
|
@ -224,23 +224,37 @@ class User(AbstractUser):
|
||||||
def has_edit_members_portfolio_permission(self, portfolio):
|
def has_edit_members_portfolio_permission(self, portfolio):
|
||||||
# BEGIN
|
# BEGIN
|
||||||
# Note code below is to add organization_request feature
|
# Note code below is to add organization_request feature
|
||||||
request = HttpRequest()
|
if not self.has_organization_members_flag():
|
||||||
request.user = self
|
|
||||||
has_organization_members_flag = flag_is_active(request, "organization_members")
|
|
||||||
if not has_organization_members_flag:
|
|
||||||
return False
|
return False
|
||||||
# END
|
# END
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
|
||||||
|
|
||||||
def has_view_all_domains_permission(self, portfolio):
|
def has_view_all_domains_portfolio_permission(self, portfolio):
|
||||||
"""Determines if the current user can view all available domains in a given portfolio"""
|
"""Determines if the current user can view all available domains in a given portfolio"""
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||||
|
|
||||||
|
def has_any_requests_portfolio_permission(self, portfolio):
|
||||||
|
# BEGIN
|
||||||
|
# Note code below is to add organization_request feature
|
||||||
|
if not self.has_organization_requests_flag():
|
||||||
|
return False
|
||||||
|
# END
|
||||||
|
return self._has_portfolio_permission(
|
||||||
|
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||||
|
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
|
def has_view_all_requests_portfolio_permission(self, portfolio):
|
||||||
|
"""Determines if the current user can view all available domain requests in a given portfolio"""
|
||||||
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
|
||||||
|
def has_edit_request_portfolio_permission(self, portfolio):
|
||||||
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
# Field specific permission checks
|
# Field specific permission checks
|
||||||
def has_view_suborganization(self, portfolio):
|
def has_view_suborganization_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
||||||
|
|
||||||
def has_edit_suborganization(self, portfolio):
|
def has_edit_suborganization_portfolio_permission(self, portfolio):
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
||||||
|
|
||||||
def get_first_portfolio(self):
|
def get_first_portfolio(self):
|
||||||
|
@ -249,36 +263,36 @@ class User(AbstractUser):
|
||||||
return permission.portfolio
|
return permission.portfolio
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def has_edit_requests(self, portfolio):
|
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
|
||||||
|
|
||||||
def portfolio_role_summary(self, portfolio):
|
def portfolio_role_summary(self, portfolio):
|
||||||
"""Returns a list of roles based on the user's permissions."""
|
"""Returns a list of roles based on the user's permissions."""
|
||||||
roles = []
|
roles = []
|
||||||
|
|
||||||
# Define the conditions and their corresponding roles
|
# Define the conditions and their corresponding roles
|
||||||
conditions_roles = [
|
conditions_roles = [
|
||||||
(self.has_edit_suborganization(portfolio), ["Admin"]),
|
(self.has_edit_suborganization_portfolio_permission(portfolio), ["Admin"]),
|
||||||
(
|
(
|
||||||
self.has_view_all_domains_permission(portfolio)
|
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||||
and self.has_domain_requests_portfolio_permission(portfolio)
|
and self.has_any_requests_portfolio_permission(portfolio)
|
||||||
and self.has_edit_requests(portfolio),
|
and self.has_edit_request_portfolio_permission(portfolio),
|
||||||
["View-only admin", "Domain requestor"],
|
["View-only admin", "Domain requestor"],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
self.has_view_all_domains_permission(portfolio)
|
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||||
and self.has_domain_requests_portfolio_permission(portfolio),
|
and self.has_any_requests_portfolio_permission(portfolio),
|
||||||
["View-only admin"],
|
["View-only admin"],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
self.has_base_portfolio_permission(portfolio)
|
self.has_base_portfolio_permission(portfolio)
|
||||||
and self.has_edit_requests(portfolio)
|
and self.has_edit_request_portfolio_permission(portfolio)
|
||||||
and self.has_domains_portfolio_permission(portfolio),
|
and self.has_any_domains_portfolio_permission(portfolio),
|
||||||
["Domain requestor", "Domain manager"],
|
["Domain requestor", "Domain manager"],
|
||||||
),
|
),
|
||||||
(self.has_base_portfolio_permission(portfolio) and self.has_edit_requests(portfolio), ["Domain requestor"]),
|
|
||||||
(
|
(
|
||||||
self.has_base_portfolio_permission(portfolio) and self.has_domains_portfolio_permission(portfolio),
|
self.has_base_portfolio_permission(portfolio) and self.has_edit_request_portfolio_permission(portfolio),
|
||||||
|
["Domain requestor"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.has_base_portfolio_permission(portfolio) and self.has_any_domains_portfolio_permission(portfolio),
|
||||||
["Domain manager"],
|
["Domain manager"],
|
||||||
),
|
),
|
||||||
(self.has_base_portfolio_permission(portfolio), ["Member"]),
|
(self.has_base_portfolio_permission(portfolio), ["Member"]),
|
||||||
|
@ -292,6 +306,9 @@ class User(AbstractUser):
|
||||||
|
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
|
def get_portfolios(self):
|
||||||
|
return self.portfolio_permissions.all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def needs_identity_verification(cls, email, uuid):
|
def needs_identity_verification(cls, email, uuid):
|
||||||
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
||||||
|
@ -437,8 +454,6 @@ class User(AbstractUser):
|
||||||
self.check_domain_invitations_on_login()
|
self.check_domain_invitations_on_login()
|
||||||
self.check_portfolio_invitations_on_login()
|
self.check_portfolio_invitations_on_login()
|
||||||
|
|
||||||
# NOTE TO DAVE: I'd simply suggest that we move these functions outside of the user object,
|
|
||||||
# and move them to some sort of utility file. That way we aren't calling request inside here.
|
|
||||||
def is_org_user(self, request):
|
def is_org_user(self, request):
|
||||||
has_organization_feature_flag = flag_is_active(request, "organization_feature")
|
has_organization_feature_flag = flag_is_active(request, "organization_feature")
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
|
@ -447,7 +462,7 @@ class User(AbstractUser):
|
||||||
def get_user_domain_ids(self, request):
|
def get_user_domain_ids(self, request):
|
||||||
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
|
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
if self.is_org_user(request) and self.has_view_all_domains_permission(portfolio):
|
if self.is_org_user(request) and self.has_view_all_domains_portfolio_permission(portfolio):
|
||||||
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
||||||
else:
|
else:
|
||||||
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
|
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from urllib.parse import urlparse, urlunparse, urlencode
|
from urllib.parse import urlparse, urlunparse, urlencode
|
||||||
|
from django.urls import resolve, Resolver404
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -315,3 +316,21 @@ def convert_queryset_to_dict(queryset, is_model=True, key="id"):
|
||||||
request_dict = {value[key]: value for value in queryset}
|
request_dict = {value[key]: value for value in queryset}
|
||||||
|
|
||||||
return request_dict
|
return request_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_url_name(path):
|
||||||
|
"""
|
||||||
|
Given a URL path, returns the corresponding URL name defined in urls.py.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The URL path to resolve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str or None: The URL name if it exists, otherwise None.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
match = resolve(path)
|
||||||
|
return match.url_name
|
||||||
|
except Resolver404:
|
||||||
|
logger.error(f"No matching URL name found for path: {path}")
|
||||||
|
return None
|
||||||
|
|
|
@ -21,7 +21,6 @@ class UserPortfolioPermissionChoices(models.TextChoices):
|
||||||
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
||||||
|
|
||||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||||
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
|
|
||||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||||
|
|
||||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||||
|
|
|
@ -9,7 +9,7 @@ class WaffleFlag(AbstractUserFlag):
|
||||||
Custom implementation of django-waffles 'Flag' object.
|
Custom implementation of django-waffles 'Flag' object.
|
||||||
Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html
|
Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html
|
||||||
|
|
||||||
Use this class when dealing with feature flags, such as profile_feature.
|
Use this class when dealing with feature flags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from registrar.models.user import User
|
from registrar.models import User
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
|
@ -72,12 +72,6 @@ class CheckUserProfileMiddleware:
|
||||||
"""Runs pre-processing logic for each view. Checks for the
|
"""Runs pre-processing logic for each view. Checks for the
|
||||||
finished_setup flag on the current user. If they haven't done so,
|
finished_setup flag on the current user. If they haven't done so,
|
||||||
then we redirect them to the finish setup page."""
|
then we redirect them to the finish setup page."""
|
||||||
# Check that the user is "opted-in" to the profile feature flag
|
|
||||||
has_profile_feature_flag = flag_is_active(request, "profile_feature")
|
|
||||||
|
|
||||||
# If they aren't, skip this check entirely
|
|
||||||
if not has_profile_feature_flag:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
profile_page = self.profile_page
|
profile_page = self.profile_page
|
||||||
|
@ -144,25 +138,30 @@ class CheckPortfolioMiddleware:
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# set the portfolio in the session if it is not set
|
# if multiple portfolios are allowed for this user
|
||||||
if "portfolio" not in request.session or request.session["portfolio"] is None:
|
if flag_is_active(request, "organization_feature"):
|
||||||
# if multiple portfolios are allowed for this user
|
self.set_portfolio_in_session(request)
|
||||||
if flag_is_active(request, "multiple_portfolios"):
|
elif request.session.get("portfolio"):
|
||||||
# NOTE: we will want to change later to have a workflow for selecting
|
# Edge case: User disables flag while already logged in
|
||||||
# portfolio and another for switching portfolio; for now, select first
|
request.session["portfolio"] = None
|
||||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
elif "portfolio" not in request.session:
|
||||||
elif flag_is_active(request, "organization_feature"):
|
# Set the portfolio in the session if its not already in it
|
||||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
request.session["portfolio"] = None
|
||||||
else:
|
|
||||||
request.session["portfolio"] = None
|
|
||||||
|
|
||||||
if request.session["portfolio"] is not None and current_path == self.home:
|
if request.user.is_org_user(request):
|
||||||
if request.user.is_org_user(request):
|
if current_path == self.home:
|
||||||
if request.user.has_domains_portfolio_permission(request.session["portfolio"]):
|
if request.user.has_any_domains_portfolio_permission(request.session["portfolio"]):
|
||||||
portfolio_redirect = reverse("domains")
|
portfolio_redirect = reverse("domains")
|
||||||
else:
|
else:
|
||||||
portfolio_redirect = reverse("no-portfolio-domains")
|
portfolio_redirect = reverse("no-portfolio-domains")
|
||||||
|
|
||||||
return HttpResponseRedirect(portfolio_redirect)
|
return HttpResponseRedirect(portfolio_redirect)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def set_portfolio_in_session(self, request):
|
||||||
|
# NOTE: we will want to change later to have a workflow for selecting
|
||||||
|
# portfolio and another for switching portfolio; for now, select first
|
||||||
|
if flag_is_active(request, "multiple_portfolios"):
|
||||||
|
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||||
|
else:
|
||||||
|
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||||
|
|
|
@ -70,7 +70,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
<div class="readonly textarea-wrapper">
|
<div class="readonly textarea-wrapper">
|
||||||
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
|
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
|
||||||
<label class="max-full" for="action_needed_reason_email_view_more">
|
<label class="max-full" for="action_needed_reason_email_view_more">
|
||||||
<strong>Sent to {% if has_profile_feature_flag %}creator{%else%}submitter{%endif%}</strong>
|
<strong>Sent to creator</strong>
|
||||||
</label>
|
</label>
|
||||||
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
||||||
{{ original_object.action_needed_reason_email }}
|
{{ original_object.action_needed_reason_email }}
|
||||||
|
@ -107,7 +107,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif field.field.name == "requested_domain" %}
|
{% elif field.field.name == "requested_domain" %}
|
||||||
{% with current_path=request.get_full_path %}
|
{% with current_path=request.get_full_path %}
|
||||||
<a class="margin-top-05 padding-top-05" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
|
<a class="margin-top-05 padding-top-05" id="id_requested_domain" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
|
||||||
{% endwith%}
|
{% endwith%}
|
||||||
{% elif field.field.name == "current_websites" %}
|
{% elif field.field.name == "current_websites" %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
|
@ -287,11 +287,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% if not skip_additional_contact_info %}
|
{% if not skip_additional_contact_info %}
|
||||||
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
|
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
|
||||||
{% endif%}
|
{% endif%}
|
||||||
{% elif field.field.name == "submitter" %}
|
|
||||||
<div class="flex-container tablet:margin-top-2">
|
|
||||||
<label aria-label="Submitter contact details"></label>
|
|
||||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
|
|
||||||
</div>
|
|
||||||
{% elif field.field.name == "senior_official" %}
|
{% elif field.field.name == "senior_official" %}
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<label aria-label="Senior official contact details"></label>
|
<label aria-label="Senior official contact details"></label>
|
||||||
|
|
|
@ -17,6 +17,26 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block after_related_objects %}
|
{% block after_related_objects %}
|
||||||
|
{% if portfolios %}
|
||||||
|
<div class="module aligned padding-3">
|
||||||
|
<h2>Portfolio information</h2>
|
||||||
|
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||||
|
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||||
|
<h3>Portfolios</h3>
|
||||||
|
<ul class="margin-0 padding-0">
|
||||||
|
{% for portfolio in portfolios %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:registrar_portfolio_change' portfolio.pk %}">
|
||||||
|
{{ portfolio }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="module aligned padding-3">
|
<div class="module aligned padding-3">
|
||||||
<h2>Associated requests and domains</h2>
|
<h2>Associated requests and domains</h2>
|
||||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||||
|
|
|
@ -72,24 +72,17 @@
|
||||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if portfolio and has_domains_portfolio_permission and has_view_suborganization %}
|
{% if portfolio and has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %}
|
||||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
{% url 'domain-suborganization' pk=domain.id as url %}
|
||||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization %}
|
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
{% url 'domain-org-name-address' pk=domain.id as url %}
|
||||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
{% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
||||||
|
|
||||||
{% url 'domain-senior-official' pk=domain.id as url %}
|
{% url 'domain-senior-official' pk=domain.id as url %}
|
||||||
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
|
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{# Conditionally display profile #}
|
|
||||||
{% if not has_profile_feature_flag %}
|
|
||||||
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
|
||||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=is_editable %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% url 'domain-security-email' pk=domain.id as url %}
|
{% url 'domain-security-email' pk=domain.id as url %}
|
||||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
|
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
<div class="grid-row margin-top-1">
|
<div class="grid-row margin-top-1">
|
||||||
<div class="grid-col">
|
<div class="grid-col">
|
||||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record">
|
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record text-secondary line-height-sans-5">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg>Delete
|
</svg>Delete
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tablet:grid-col-2">
|
<div class="tablet:grid-col-2">
|
||||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075">
|
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075 text-secondary line-height-sans-5">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg>Delete
|
</svg>Delete
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{# this is right after the messages block in the parent template #}
|
{# this is right after the messages block in the parent template #}
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
<h1>Organization name and mailing address </h1>
|
<h1>Organization</h1>
|
||||||
|
|
||||||
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,26 @@
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||||
</svg><span class="margin-left-05">Previous step</span>
|
</svg><span class="margin-left-05">Previous step</span>
|
||||||
</a>
|
</a>
|
||||||
|
{% comment %}
|
||||||
|
TODO: uncomment in #2596
|
||||||
|
{% else %}
|
||||||
|
{% if portfolio %}
|
||||||
|
{% url 'domain-requests' as url_2 %}
|
||||||
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||||
|
<ol class="usa-breadcrumb__list">
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
<a href="{{ url_2 }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||||
|
{% if requested_domain__name %}
|
||||||
|
<span>{{ requested_domain__name }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span>Start a new domain request</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% endif %} {% endcomment %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block form_messages %}
|
{% block form_messages %}
|
||||||
|
|
|
@ -16,11 +16,9 @@
|
||||||
<h2>Time to complete the form</h2>
|
<h2>Time to complete the form</h2>
|
||||||
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
|
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
|
||||||
completing your domain request might take around 15 minutes.</p>
|
completing your domain request might take around 15 minutes.</p>
|
||||||
{% if has_profile_feature_flag %}
|
|
||||||
<h2>How we’ll reach you</h2>
|
<h2>How we’ll reach you</h2>
|
||||||
<p>While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
|
<p>While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
|
||||||
{% include "includes/profile_information.html" with user=user%}
|
{% include "includes/profile_information.html" with user=user%}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block form_buttons %}
|
{% block form_buttons %}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2">
|
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2 text-secondary line-height-sans-5">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg><span class="margin-left-05">Delete</span>
|
</svg><span class="margin-left-05">Delete</span>
|
||||||
|
|
|
@ -130,8 +130,8 @@
|
||||||
|
|
||||||
{% if step == Step.YOUR_CONTACT %}
|
{% if step == Step.YOUR_CONTACT %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% if domain_request.submitter is not None %}
|
{% if domain_request.creator is not None %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.submitter %}
|
{% with title=form_titles|get_item:step value=domain_request.creator %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -8,15 +8,30 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||||
<a href="{% url 'home' %}" class="breadcrumb__back">
|
{% if portfolio %}
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
{% url 'domain-requests' as url %}
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
{% else %}
|
||||||
</svg>
|
{% url 'home' as url %}
|
||||||
|
{% endif %}
|
||||||
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||||
|
<ol class="usa-breadcrumb__list">
|
||||||
|
<li class="usa-breadcrumb__list-item">
|
||||||
|
{% if portfolio %}
|
||||||
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||||
|
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
|
||||||
|
<span>New domain request</span>
|
||||||
|
{% else %}
|
||||||
|
<span>{{ DomainRequest.requested_domain.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<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>
|
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||||
|
@ -30,18 +45,63 @@
|
||||||
<span class="text-bold text-primary-darker">
|
<span class="text-bold text-primary-darker">
|
||||||
Status:
|
Status:
|
||||||
</span>
|
</span>
|
||||||
{% if DomainRequest.status == 'approved' %} Approved
|
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
||||||
{% 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<p><b class="review__step__name">Last updated:</b> {{DomainRequest.updated_at|date:"F j, Y"}}</p>
|
|
||||||
|
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
|
||||||
|
{% comment %}
|
||||||
|
These are intentionally seperated this way.
|
||||||
|
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
||||||
|
Leave it this way until we've solidified our requirements.
|
||||||
|
{% endcomment %}
|
||||||
|
{% if DomainRequest.status == statuses.STARTED %}
|
||||||
|
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
||||||
|
<p class="margin-top-1">
|
||||||
|
{% comment %}
|
||||||
|
A newly created domain request will not have a value for last_status update.
|
||||||
|
This is because the status never really updated.
|
||||||
|
However, if this somehow goes back to started we can default to displaying that new date.
|
||||||
|
{% endcomment %}
|
||||||
|
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
|
||||||
|
</p>
|
||||||
|
{% endwith %}
|
||||||
|
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
||||||
|
<p class="margin-top-1 margin-bottom-1">
|
||||||
|
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||||
|
</p>
|
||||||
|
<p class="margin-top-1">
|
||||||
|
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
|
</p>
|
||||||
|
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
||||||
|
<p class="margin-top-1 margin-bottom-1">
|
||||||
|
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||||
|
</p>
|
||||||
|
<p class="margin-top-1">
|
||||||
|
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
|
</p>
|
||||||
|
{% elif DomainRequest.status == statuses.REJECTED %}
|
||||||
|
<p class="margin-top-1 margin-bottom-1">
|
||||||
|
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||||
|
</p>
|
||||||
|
<p class="margin-top-1">
|
||||||
|
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
|
||||||
|
</p>
|
||||||
|
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
||||||
|
<p class="margin-top-1 margin-bottom-1">
|
||||||
|
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||||
|
</p>
|
||||||
|
<p class="margin-top-1">
|
||||||
|
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
||||||
|
<p class="margin-top-1">
|
||||||
|
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.status != 'rejected' %}
|
{% if DomainRequest.status != 'rejected' %}
|
||||||
<p>{% include "includes/domain_request.html" %}</p>
|
<p>{% include "includes/domain_request.html" %}</p>
|
||||||
|
@ -49,6 +109,7 @@
|
||||||
Withdraw request</a>
|
Withdraw request</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||||
|
@ -82,7 +143,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.organization_name %}
|
{% if DomainRequest.organization_name %}
|
||||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=DomainRequest address='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.about_your_organization %}
|
{% if DomainRequest.about_your_organization %}
|
||||||
|
@ -109,10 +170,9 @@
|
||||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.submitter and not has_profile_feature_flag %}
|
{% if DomainRequest.creator %}
|
||||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.submitter contact='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.other_contacts.all %}
|
{% 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 %}
|
{% 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 %}
|
{% else %}
|
||||||
|
@ -123,8 +183,8 @@
|
||||||
{% if DomainRequest %}
|
{% if DomainRequest %}
|
||||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if domain_request.cisa_representative_first_name %}
|
{% if DomainRequest.cisa_representative_first_name %}
|
||||||
{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}
|
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
No
|
No
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
{% if not portfolio %}
|
{% if not portfolio %}
|
||||||
{% with url_name="domain-org-name-address" %}
|
{% with url_name="domain-org-name-address" %}
|
||||||
{% include "includes/domain_sidenav_item.html" with item_text="Organization name and mailing address" %}
|
{% include "includes/domain_sidenav_item.html" with item_text="Organization" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
{% comment %} Only show this menu option if the user has the perms to do so {% endcomment %}
|
{% comment %} Only show this menu option if the user has the perms to do so {% endcomment %}
|
||||||
{% if has_domains_portfolio_permission and has_view_suborganization %}
|
{% if has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %}
|
||||||
{% with url_name="domain-suborganization" %}
|
{% with url_name="domain-suborganization" %}
|
||||||
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
|
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -72,13 +72,6 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not has_profile_feature_flag %}
|
|
||||||
{# Conditionally display profile link in main nav #}
|
|
||||||
{% with url_name="domain-your-contact-information" %}
|
|
||||||
{% include "includes/domain_sidenav_item.html" with item_text="Your contact information" %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% with url_name="domain-security-email" %}
|
{% with url_name="domain-security-email" %}
|
||||||
{% include "includes/domain_sidenav_item.html" with item_text="Security email" %}
|
{% include "includes/domain_sidenav_item.html" with item_text="Security email" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if has_domains_portfolio_permission and has_edit_suborganization %}
|
{% if has_any_domains_portfolio_permission and has_edit_suborganization_portfolio_permission %}
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% input_with_errors form.sub_organization %}
|
{% input_with_errors form.sub_organization %}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
{% extends "domain_base.html" %}
|
|
||||||
{% load static field_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Your contact information | {{ domain.name }} | {% endblock %}
|
|
||||||
|
|
||||||
{% block domain_content %}
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
<h1>Your contact information</h1>
|
|
||||||
|
|
||||||
<p>If you’d like us to use a different name, email, or phone number you can make those changes below. <strong>Updating your contact information here will update the contact information for all domains in your account.</strong> Changing your information here won’t affect your Login.gov account information. The contact information you provide here won’t be made public and will only be used for the .gov program.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
{% input_with_errors form.first_name %}
|
|
||||||
|
|
||||||
{% input_with_errors form.middle_name %}
|
|
||||||
|
|
||||||
{% input_with_errors form.last_name %}
|
|
||||||
|
|
||||||
{% input_with_errors form.title %}
|
|
||||||
|
|
||||||
{% input_with_errors form.email %}
|
|
||||||
|
|
||||||
{% input_with_errors form.phone %}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="usa-button"
|
|
||||||
>Save</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %} {# domain_content #}
|
|
|
@ -15,7 +15,7 @@ State-recognized tribe
|
||||||
Election office:
|
Election office:
|
||||||
{{ domain_request.is_election_board|yesno:"Yes,No,Incomplete" }}
|
{{ domain_request.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
Organization name and mailing address:
|
Organization:
|
||||||
{% spaceless %}{{ domain_request.federal_agency }}
|
{% spaceless %}{{ domain_request.federal_agency }}
|
||||||
{{ domain_request.organization_name }}
|
{{ domain_request.organization_name }}
|
||||||
{{ domain_request.address_line1 }}{% if domain_request.address_line2 %}
|
{{ domain_request.address_line1 }}{% if domain_request.address_line2 %}
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
||||||
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
|
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
{% if not has_domain_requests_portfolio_permission %}
|
{% if not portfolio %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- Embedding the portfolio value in a data attribute -->
|
||||||
|
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
||||||
|
@ -20,13 +23,21 @@
|
||||||
</svg>
|
</svg>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
|
{% if portfolio %}
|
||||||
|
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name or creator</label>
|
||||||
|
{% else %}
|
||||||
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
||||||
|
{% endif %}
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="domain-requests__search-field"
|
id="domain-requests__search-field"
|
||||||
type="search"
|
type="search"
|
||||||
name="search"
|
name="search"
|
||||||
|
{% if portfolio %}
|
||||||
|
placeholder="Search by domain name or creator"
|
||||||
|
{% else %}
|
||||||
placeholder="Search by domain name"
|
placeholder="Search by domain name"
|
||||||
|
{% endif %}
|
||||||
/>
|
/>
|
||||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
||||||
<img
|
<img
|
||||||
|
@ -39,13 +50,135 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if portfolio %}
|
||||||
|
<div class="display-flex flex-align-center">
|
||||||
|
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
||||||
|
<div class="usa-accordion usa-accordion--select margin-right-2">
|
||||||
|
<div class="usa-accordion__heading">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--small padding--8-8-9 usa-button--outline usa-button--filter usa-accordion__button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="filter-status"
|
||||||
|
>
|
||||||
|
<span class="filter-indicator text-bold display-none"></span> Status
|
||||||
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="filter-status" class="usa-accordion__content usa-prose shadow-1">
|
||||||
|
<h2>Status</h2>
|
||||||
|
<fieldset class="usa-fieldset margin-top-0">
|
||||||
|
<legend class="usa-legend">Select to apply <span class="sr-only">status</span> filter</legend>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-started"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="started"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-started"
|
||||||
|
>Started</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-submitted"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="submitted"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-submitted"
|
||||||
|
>Submitted</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-in-review"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="in review"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-in-review"
|
||||||
|
>In review</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-action-needed"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="action needed"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-action-needed"
|
||||||
|
>Action needed</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-rejected"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="rejected"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-rejected"
|
||||||
|
>Rejected</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-withdrawn"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="withdrawn"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-withdrawn"
|
||||||
|
>Withdrawn</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-ineligible"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="ineligible"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-ineligible"
|
||||||
|
>Ineligible</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domain-requests__reset-filters display-none"
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
|
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Date submitted</th>
|
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Submitted</th>
|
||||||
|
{% if portfolio %}
|
||||||
|
<th data-sortable="creator" scope="col" role="columnheader">Created by</th>
|
||||||
|
{% endif %}
|
||||||
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||||
<!-- AJAX will conditionally add a th for delete actions -->
|
<!-- AJAX will conditionally add a th for delete actions -->
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||||
{% if not portfolio %}
|
{% if not portfolio %}
|
||||||
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
||||||
<span class="display-none" id="no-portfolio-js-flag"></span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Embedding the portfolio value in a data attribute -->
|
<!-- Embedding the portfolio value in a data attribute -->
|
||||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||||
|
@ -65,7 +64,7 @@
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="filter-status"
|
aria-controls="filter-status"
|
||||||
>
|
>
|
||||||
<span class="domain__filter-indicator text-bold display-none"></span> Status
|
<span class="filter-indicator text-bold display-none"></span> Status
|
||||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -157,7 +156,7 @@
|
||||||
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
|
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
||||||
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
|
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
|
||||||
{% if portfolio and has_view_suborganization %}
|
{% if portfolio and has_view_suborganization_portfolio_permission %}
|
||||||
<th data-sortable="domain_info__sub_organization" scope="col" role="columnheader">Suborganization</th>
|
<th data-sortable="domain_info__sub_organization" scope="col" role="columnheader">Suborganization</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th
|
<th
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<span class="usa-nav__username ellipsis">{{ user.email }}</span>
|
<span class="usa-nav__username ellipsis">{{ user.email }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% if has_profile_feature_flag %}
|
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% url 'user-profile' as user_profile_url %}
|
{% url 'user-profile' as user_profile_url %}
|
||||||
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
||||||
|
@ -24,7 +23,6 @@
|
||||||
<span class="text-primary">Your profile</span>
|
<span class="text-primary">Your profile</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
|
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<span class="ellipsis usa-nav__username">{{ user.email }}</span>
|
<span class="ellipsis usa-nav__username">{{ user.email }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% if has_profile_feature_flag %}
|
|
||||||
<li class="usa-nav__secondary-item">
|
<li class="usa-nav__secondary-item">
|
||||||
{% url 'user-profile' as user_profile_url %}
|
{% url 'user-profile' as user_profile_url %}
|
||||||
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
||||||
|
@ -26,7 +25,6 @@
|
||||||
Your profile
|
Your profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
<li class="usa-nav__secondary-item">
|
<li class="usa-nav__secondary-item">
|
||||||
<a class="usa-nav-link" href="{% url 'logout' %}">Sign out</a>
|
<a class="usa-nav-link" href="{% url 'logout' %}">Sign out</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -37,12 +35,12 @@
|
||||||
</div>
|
</div>
|
||||||
<ul class="usa-nav__primary usa-accordion">
|
<ul class="usa-nav__primary usa-accordion">
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% if has_domains_portfolio_permission %}
|
{% if has_any_domains_portfolio_permission %}
|
||||||
{% url 'domains' as url %}
|
{% url 'domains' as url %}
|
||||||
{%else %}
|
{% else %}
|
||||||
{% url 'no-portfolio-domains' as url %}
|
{% url 'no-portfolio-domains' as url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
|
<a href="{{ url }}" class="usa-nav-link{% if path|is_domain_subpage %} usa-current{% endif %}">
|
||||||
Domains
|
Domains
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -52,25 +50,59 @@
|
||||||
</a>
|
</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
|
|
||||||
{% if has_domain_requests_portfolio_permission %}
|
{% if has_organization_requests_flag %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
|
<!-- user has one of the view permissions plus the edit permission, show the dropdown -->
|
||||||
|
{% if has_edit_request_portfolio_permission %}
|
||||||
{% url 'domain-requests' as url %}
|
{% url 'domain-requests' as url %}
|
||||||
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-accordion__button usa-nav__link{% if path|is_domain_request_subpage %} usa-current{% endif %}"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="basic-nav-section-two"
|
||||||
|
>
|
||||||
|
<span>Domain requests</span>
|
||||||
|
</button>
|
||||||
|
<ul id="basic-nav-section-two" class="usa-nav__submenu">
|
||||||
|
<li class="usa-nav__submenu-item">
|
||||||
|
<a href="{{ url }}"
|
||||||
|
><span>Domain requests</span></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="usa-nav__submenu-item">
|
||||||
|
<a href="{% url 'domain-request:' %}"
|
||||||
|
><span>Start a new domain request</span></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<!-- user has view but no edit permissions -->
|
||||||
|
{% elif has_any_requests_portfolio_permission %}
|
||||||
|
{% url 'domain-requests' as url %}
|
||||||
|
<a href="{{ url }}" class="usa-nav-link{% if path|is_domain_request_subpage %} usa-current{% endif %}">
|
||||||
|
Domain requests
|
||||||
|
</a>
|
||||||
|
<!-- user does not have permissions -->
|
||||||
|
{% else %}
|
||||||
|
{% url 'no-portfolio-requests' as url %}
|
||||||
|
<a href="{{ url }}" class="usa-nav-link{% if path|is_domain_request_subpage %} usa-current{% endif %}">
|
||||||
Domain requests
|
Domain requests
|
||||||
</a>
|
</a>
|
||||||
</li>
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if has_view_members_portfolio_permission %}
|
|
||||||
|
{% if has_organization_members_flag %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
<a href="#" class="usa-nav-link">
|
<a href="#" class="usa-nav-link">
|
||||||
Members
|
Members
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% url 'organization' as url %}
|
{% url 'organization' as url %}
|
||||||
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
||||||
<a href="{{ url }}" class="usa-nav-link padding-y-0 {% if request.path == '/organization/' %} usa-current{% endif %}">
|
<a href="{{ url }}" class="usa-nav-link padding-y-0 {% if path|is_portfolio_subpage %} usa-current{% endif %}">
|
||||||
<span class="ellipsis ellipsis--23 ellipsis--desktop-50 padding-y-1 desktop:padding-y-2">
|
<span class="ellipsis ellipsis--23 ellipsis--desktop-50 padding-y-1 desktop:padding-y-2">
|
||||||
{{ portfolio.organization_name }}
|
{{ portfolio.organization_name }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
<div class="usa-alert">
|
<div class="usa-alert">
|
||||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
|
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
|
||||||
<b>Attention:</b> You are on a test site.
|
<b>Attention:</b> You are on a test site.
|
||||||
{% if has_profile_feature_flag %}
|
|
||||||
The profile_feature flag is active.
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,15 +16,7 @@
|
||||||
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
{% else %}
|
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate id="form-container">
|
||||||
<p>
|
|
||||||
The senior official for your organization can’t be updated here.
|
|
||||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if can_edit %}
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% input_with_errors form.first_name %}
|
{% input_with_errors form.first_name %}
|
||||||
{% input_with_errors form.last_name %}
|
{% input_with_errors form.last_name %}
|
||||||
|
@ -33,17 +25,26 @@
|
||||||
<button type="submit" class="usa-button">Save</button>
|
<button type="submit" class="usa-button">Save</button>
|
||||||
</form>
|
</form>
|
||||||
{% elif not form.full_name.value and not form.title.value and not form.email.value %}
|
{% elif not form.full_name.value and not form.title.value and not form.email.value %}
|
||||||
<h4>No senior official was found.</h4>
|
<p>
|
||||||
|
Your senior official is a person within your organization who can authorize domain requests.
|
||||||
|
We don't have information about your organization's senior official. To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if form.full_name.value is not None %}
|
<p>
|
||||||
{% include "includes/input_read_only.html" with field=form.full_name %}
|
The senior official for your organization can’t be updated here.
|
||||||
{% endif %}
|
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
|
</p>
|
||||||
|
<div class="desktop:margin-top-4">
|
||||||
|
{% if form.full_name.value is not None %}
|
||||||
|
{% include "includes/input_read_only.html" with field=form.full_name %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if form.title.value is not None %}
|
{% if form.title.value is not None %}
|
||||||
{% include "includes/input_read_only.html" with field=form.title %}
|
{% include "includes/input_read_only.html" with field=form.title %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if form.email.value is not None %}
|
{% if form.email.value is not None %}
|
||||||
{% include "includes/input_read_only.html" with field=form.email %}
|
{% include "includes/input_read_only.html" with field=form.email %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -9,9 +9,6 @@
|
||||||
{# the entire logged in page goes here #}
|
{# the entire logged in page goes here #}
|
||||||
|
|
||||||
<div class="tablet:grid-col-12">
|
<div class="tablet:grid-col-12">
|
||||||
{% block messages %}
|
|
||||||
{% include "includes/form_messages.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block portfolio_content %}{% endblock %}
|
{% block portfolio_content %}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
{% block messages %}
|
||||||
|
{% include "includes/form_messages.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<h1 id="domains-header">Domains</h1>
|
<h1 id="domains-header">Domains</h1>
|
||||||
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
|
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
|
||||||
|
|
|
@ -5,6 +5,12 @@
|
||||||
{% block title %} Domains | {% endblock %}
|
{% block title %} Domains | {% endblock %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
|
{% block messages %}
|
||||||
|
{% include "includes/form_messages.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<h1 id="domains-header">Domains</h1>
|
<h1 id="domains-header">Domains</h1>
|
||||||
<section class="section-outlined">
|
<section class="section-outlined">
|
30
src/registrar/templates/portfolio_no_requests.html
Normal file
30
src/registrar/templates/portfolio_no_requests.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends 'portfolio_base.html' %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %} Domain Requests | {% endblock %}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
<h1 id="domains-header">Current domain requests</h1>
|
||||||
|
<section class="section-outlined">
|
||||||
|
<div class="section-outlined__header margin-bottom-3">
|
||||||
|
<h2 id="domains-header" class="display-inline-block">You don’t have access to domain requests.</h2>
|
||||||
|
{% if portfolio_administrators %}
|
||||||
|
<p>If you believe you should have access to a request, reach out to your organization’s administrators.</p>
|
||||||
|
<p>Your organizations administrators:</p>
|
||||||
|
<ul class="margin-top-0">
|
||||||
|
{% for administrator in portfolio_administrators %}
|
||||||
|
{% if administrator.email %}
|
||||||
|
<li>{{ administrator.email }}</li>
|
||||||
|
{% else %}
|
||||||
|
<li>{{ administrator }}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p><strong>No administrators were found on your organization.</strong></p>
|
||||||
|
<p>If you believe you should have access to a request, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'portfolio_base.html' %}
|
{% extends 'portfolio_base.html' %}
|
||||||
{% load static field_helpers%}
|
{% load static field_helpers%}
|
||||||
|
|
||||||
{% block title %}Organization mailing address | {{ portfolio.name }}{% endblock %}
|
{% block title %}Organization name and mailing address | {{ portfolio.name }}{% endblock %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
@ -19,21 +19,25 @@
|
||||||
|
|
||||||
<div class="tablet:grid-col-9" id="main-content">
|
<div class="tablet:grid-col-9" id="main-content">
|
||||||
|
|
||||||
|
{% block messages %}
|
||||||
|
{% include "includes/form_messages.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<h1>Organization</h1>
|
<h1>Organization</h1>
|
||||||
|
|
||||||
<p>The name of your federal agency will be publicly listed as the domain registrant.</p>
|
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
||||||
|
|
||||||
{% if has_edit_org_portfolio_permission %}
|
{% if has_edit_org_portfolio_permission %}
|
||||||
<p>
|
<p>
|
||||||
The federal agency for your organization can’t be updated here.
|
Your organization name can’t be updated here.
|
||||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h4 class="read-only-label">Federal agency</h4>
|
<h4 class="read-only-label">Organization name</h4>
|
||||||
<p class="read-only-value">
|
<p class="read-only-value">
|
||||||
{{ portfolio.federal_agency }}
|
{{ portfolio.federal_agency }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -49,7 +53,7 @@
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h4 class="read-only-label">Federal agency</h4>
|
<h4 class="read-only-label">Organization name</h4>
|
||||||
<p class="read-only-value">
|
<p class="read-only-value">
|
||||||
{{ portfolio.federal_agency }}
|
{{ portfolio.federal_agency }}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -9,20 +9,36 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
{% block messages %}
|
||||||
|
{% include "includes/form_messages.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<h1 id="domain-requests-header">Domain requests</h1>
|
<h1 id="domain-requests-header">Domain requests</h1>
|
||||||
|
<div class="grid-row grid-gap">
|
||||||
|
|
||||||
|
{% if has_edit_request_portfolio_permission %}
|
||||||
|
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||||
|
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||||
|
{% comment %}
|
||||||
|
IMPORTANT:
|
||||||
|
If this button is added on any other page, make sure to update the
|
||||||
|
relevant view to reset request.session["new_request"] = True
|
||||||
|
{% endcomment %}
|
||||||
|
<p class="float-right-tablet tablet:margin-y-0">
|
||||||
|
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||||
|
>
|
||||||
|
Start a new domain request
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
IMPORTANT:
|
|
||||||
If this button is added on any other page, make sure to update the
|
|
||||||
relevant view to reset request.session["new_request"] = True
|
|
||||||
{% endcomment %}
|
|
||||||
<p class="margin-top-4">
|
|
||||||
<a href="{% url 'domain-request:' %}" class="usa-button"
|
|
||||||
>
|
|
||||||
Start a new domain request
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
{% block messages %}
|
||||||
|
{% include "includes/form_messages.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div class="grid-row grid-gap">
|
<div class="grid-row grid-gap">
|
||||||
<div class="tablet:grid-col-3">
|
<div class="tablet:grid-col-3">
|
||||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||||
|
|
|
@ -3,6 +3,9 @@ from django import template
|
||||||
import re
|
import re
|
||||||
from registrar.models.domain_request import DomainRequest
|
from registrar.models.domain_request import DomainRequest
|
||||||
from phonenumber_field.phonenumber import PhoneNumber
|
from phonenumber_field.phonenumber import PhoneNumber
|
||||||
|
from registrar.views.domain_request import DomainRequestWizard
|
||||||
|
|
||||||
|
from registrar.models.utility.generic_helper import get_url_name
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -174,3 +177,65 @@ def has_contact_info(user):
|
||||||
@register.filter
|
@register.filter
|
||||||
def model_name_lowercase(instance):
|
def model_name_lowercase(instance):
|
||||||
return instance.__class__.__name__.lower()
|
return instance.__class__.__name__.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="is_domain_subpage")
|
||||||
|
def is_domain_subpage(path):
|
||||||
|
"""Checks if the given page is a subpage of domains.
|
||||||
|
Takes a path name, like '/domains/'."""
|
||||||
|
# Since our pages aren't unified under a common path, we need this approach for now.
|
||||||
|
url_names = [
|
||||||
|
"domains",
|
||||||
|
"no-portfolio-domains",
|
||||||
|
"domain",
|
||||||
|
"domain-users",
|
||||||
|
"domain-dns",
|
||||||
|
"domain-dns-nameservers",
|
||||||
|
"domain-dns-dnssec",
|
||||||
|
"domain-dns-dnssec-dsdata",
|
||||||
|
"domain-your-contact-information",
|
||||||
|
"domain-org-name-address",
|
||||||
|
"domain-senior-official",
|
||||||
|
"domain-security-email",
|
||||||
|
"domain-users-add",
|
||||||
|
"domain-request-delete",
|
||||||
|
"domain-user-delete",
|
||||||
|
"invitation-delete",
|
||||||
|
]
|
||||||
|
return get_url_name(path) in url_names
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="is_domain_request_subpage")
|
||||||
|
def is_domain_request_subpage(path):
|
||||||
|
"""Checks if the given page is a subpage of domain requests.
|
||||||
|
Takes a path name, like '/requests/'."""
|
||||||
|
# Since our pages aren't unified under a common path, we need this approach for now.
|
||||||
|
url_names = [
|
||||||
|
"domain-requests",
|
||||||
|
"no-portfolio-requests",
|
||||||
|
"domain-request-status",
|
||||||
|
"domain-request-withdraw-confirmation",
|
||||||
|
"domain-request-withdrawn",
|
||||||
|
"domain-request-delete",
|
||||||
|
]
|
||||||
|
|
||||||
|
# The domain request wizard pages don't have a defined path,
|
||||||
|
# so we need to check directly on it.
|
||||||
|
wizard_paths = [
|
||||||
|
DomainRequestWizard.EDIT_URL_NAME,
|
||||||
|
DomainRequestWizard.URL_NAMESPACE,
|
||||||
|
DomainRequestWizard.NEW_URL_NAME,
|
||||||
|
]
|
||||||
|
return get_url_name(path) in url_names or any(wizard in path for wizard in wizard_paths)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="is_portfolio_subpage")
|
||||||
|
def is_portfolio_subpage(path):
|
||||||
|
"""Checks if the given page is a subpage of portfolio.
|
||||||
|
Takes a path name, like '/organization/'."""
|
||||||
|
# Since our pages aren't unified under a common path, we need this approach for now.
|
||||||
|
url_names = [
|
||||||
|
"organization",
|
||||||
|
"senior-official",
|
||||||
|
]
|
||||||
|
return get_url_name(path) in url_names
|
||||||
|
|
|
@ -394,7 +394,6 @@ class AuditedAdminMockData:
|
||||||
about_your_organization: str = "e-Government",
|
about_your_organization: str = "e-Government",
|
||||||
anything_else: str = "There is more",
|
anything_else: str = "There is more",
|
||||||
senior_official: Contact = self.dummy_contact(item_name, "senior_official"),
|
senior_official: Contact = self.dummy_contact(item_name, "senior_official"),
|
||||||
submitter: Contact = self.dummy_contact(item_name, "submitter"),
|
|
||||||
creator: User = self.dummy_user(item_name, "creator"),
|
creator: User = self.dummy_user(item_name, "creator"),
|
||||||
}
|
}
|
||||||
""" # noqa
|
""" # noqa
|
||||||
|
@ -412,7 +411,6 @@ class AuditedAdminMockData:
|
||||||
about_your_organization="e-Government",
|
about_your_organization="e-Government",
|
||||||
anything_else="There is more",
|
anything_else="There is more",
|
||||||
senior_official=self.dummy_contact(item_name, "senior_official"),
|
senior_official=self.dummy_contact(item_name, "senior_official"),
|
||||||
submitter=self.dummy_contact(item_name, "submitter"),
|
|
||||||
creator=creator,
|
creator=creator,
|
||||||
)
|
)
|
||||||
return common_args
|
return common_args
|
||||||
|
@ -537,8 +535,10 @@ class MockDb(TestCase):
|
||||||
first_name = "First"
|
first_name = "First"
|
||||||
last_name = "Last"
|
last_name = "Last"
|
||||||
email = "info@example.com"
|
email = "info@example.com"
|
||||||
|
title = "title"
|
||||||
|
phone = "8080102431"
|
||||||
cls.user = get_user_model().objects.create(
|
cls.user = get_user_model().objects.create(
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email
|
username=username, first_name=first_name, last_name=last_name, email=email, title=title, phone=phone
|
||||||
)
|
)
|
||||||
|
|
||||||
current_date = get_time_aware_date(datetime(2024, 4, 2))
|
current_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
|
@ -847,6 +847,7 @@ def create_superuser():
|
||||||
last_name="last",
|
last_name="last",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
password=p,
|
password=p,
|
||||||
|
phone="8003111234",
|
||||||
)
|
)
|
||||||
# Retrieve the group or create it if it doesn't exist
|
# Retrieve the group or create it if it doesn't exist
|
||||||
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
|
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
|
||||||
|
@ -864,7 +865,9 @@ def create_user():
|
||||||
first_name="first",
|
first_name="first",
|
||||||
last_name="last",
|
last_name="last",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
|
title="title",
|
||||||
password=p,
|
password=p,
|
||||||
|
phone="8003111234",
|
||||||
)
|
)
|
||||||
# Retrieve the group or create it if it doesn't exist
|
# Retrieve the group or create it if it doesn't exist
|
||||||
group, _ = UserGroup.objects.get_or_create(name="cisa_analysts_group")
|
group, _ = UserGroup.objects.get_or_create(name="cisa_analysts_group")
|
||||||
|
@ -881,7 +884,12 @@ def create_test_user():
|
||||||
phone = "8003111234"
|
phone = "8003111234"
|
||||||
title = "test title"
|
title = "test title"
|
||||||
user = get_user_model().objects.create(
|
user = get_user_model().objects.create(
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, title=title
|
username=username,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
email=email,
|
||||||
|
phone=phone,
|
||||||
|
title=title,
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
@ -901,7 +909,6 @@ def completed_domain_request( # noqa
|
||||||
has_cisa_representative=True,
|
has_cisa_representative=True,
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
user=False,
|
user=False,
|
||||||
submitter=False,
|
|
||||||
name="city.gov",
|
name="city.gov",
|
||||||
investigator=None,
|
investigator=None,
|
||||||
generic_org_type="federal",
|
generic_org_type="federal",
|
||||||
|
@ -911,6 +918,7 @@ def completed_domain_request( # noqa
|
||||||
federal_type=None,
|
federal_type=None,
|
||||||
action_needed_reason=None,
|
action_needed_reason=None,
|
||||||
portfolio=None,
|
portfolio=None,
|
||||||
|
organization_name=None,
|
||||||
):
|
):
|
||||||
"""A completed domain request."""
|
"""A completed domain request."""
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -925,14 +933,6 @@ def completed_domain_request( # noqa
|
||||||
domain, _ = DraftDomain.objects.get_or_create(name=name)
|
domain, _ = DraftDomain.objects.get_or_create(name=name)
|
||||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||||
current, _ = Website.objects.get_or_create(website="city.com")
|
current, _ = Website.objects.get_or_create(website="city.com")
|
||||||
if not submitter:
|
|
||||||
submitter, _ = Contact.objects.get_or_create(
|
|
||||||
first_name="Testy2",
|
|
||||||
last_name="Tester2",
|
|
||||||
title="Admin Tester",
|
|
||||||
email="mayor@igorville.gov",
|
|
||||||
phone="(555) 555 5556",
|
|
||||||
)
|
|
||||||
other, _ = Contact.objects.get_or_create(
|
other, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy",
|
first_name="Testy",
|
||||||
last_name="Tester",
|
last_name="Tester",
|
||||||
|
@ -954,14 +954,13 @@ def completed_domain_request( # noqa
|
||||||
federal_type="executive",
|
federal_type="executive",
|
||||||
purpose="Purpose of the site",
|
purpose="Purpose of the site",
|
||||||
is_policy_acknowledged=True,
|
is_policy_acknowledged=True,
|
||||||
organization_name="Testorg",
|
organization_name=organization_name if organization_name else "Testorg",
|
||||||
address_line1="address 1",
|
address_line1="address 1",
|
||||||
address_line2="address 2",
|
address_line2="address 2",
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
requested_domain=domain,
|
requested_domain=domain,
|
||||||
submitter=submitter,
|
|
||||||
creator=user,
|
creator=user,
|
||||||
status=status,
|
status=status,
|
||||||
investigator=investigator,
|
investigator=investigator,
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django_webtest import WebTest # type: ignore
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from registrar.admin import (
|
from registrar.admin import (
|
||||||
|
@ -41,13 +42,12 @@ from registrar.models import (
|
||||||
TransitionDomain,
|
TransitionDomain,
|
||||||
Portfolio,
|
Portfolio,
|
||||||
Suborganization,
|
Suborganization,
|
||||||
|
UserPortfolioPermission,
|
||||||
|
UserDomainRole,
|
||||||
|
SeniorOfficial,
|
||||||
|
PortfolioInvitation,
|
||||||
|
VerifiedByStaff,
|
||||||
)
|
)
|
||||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
|
||||||
from registrar.models.senior_official import SeniorOfficial
|
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
|
||||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
|
||||||
from .common import (
|
from .common import (
|
||||||
MockDbForSharedTests,
|
MockDbForSharedTests,
|
||||||
AuditedAdminMockData,
|
AuditedAdminMockData,
|
||||||
|
@ -60,10 +60,12 @@ from .common import (
|
||||||
multiple_unalphabetical_domain_objects,
|
multiple_unalphabetical_domain_objects,
|
||||||
GenericTestHelper,
|
GenericTestHelper,
|
||||||
)
|
)
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from unittest.mock import ANY, patch, Mock
|
from unittest.mock import ANY, patch, Mock
|
||||||
from django_webtest import WebTest # type: ignore
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -383,7 +385,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
|
|
||||||
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
||||||
domain_request = completed_domain_request(
|
domain_request = completed_domain_request(
|
||||||
submitter=contact, name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||||
)
|
)
|
||||||
domain_request.approve()
|
domain_request.approve()
|
||||||
|
|
||||||
|
@ -508,7 +510,6 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
# These should exist in the response
|
# These should exist in the response
|
||||||
expected_values = [
|
expected_values = [
|
||||||
("creator", "Person who submitted the domain request"),
|
("creator", "Person who submitted the domain request"),
|
||||||
("submitter", 'Person listed under "your contact information" in the request form'),
|
|
||||||
("domain_request", "Request associated with this domain"),
|
("domain_request", "Request associated with this domain"),
|
||||||
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
||||||
("urbanization", "Required for Puerto Rico only"),
|
("urbanization", "Required for Puerto Rico only"),
|
||||||
|
@ -632,16 +633,6 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
# Check for the field itself
|
# Check for the field itself
|
||||||
self.assertContains(response, "Meoward Jones")
|
self.assertContains(response, "Meoward Jones")
|
||||||
|
|
||||||
# == Check for the submitter == #
|
|
||||||
self.assertContains(response, "mayor@igorville.gov", count=2)
|
|
||||||
expected_submitter_fields = [
|
|
||||||
# Field, expected value
|
|
||||||
("title", "Admin Tester"),
|
|
||||||
("phone", "(555) 555 5556"),
|
|
||||||
]
|
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
|
|
||||||
self.assertContains(response, "Testy2 Tester2")
|
|
||||||
|
|
||||||
# == Check for the senior_official == #
|
# == Check for the senior_official == #
|
||||||
self.assertContains(response, "testy@town.com", count=2)
|
self.assertContains(response, "testy@town.com", count=2)
|
||||||
expected_so_fields = [
|
expected_so_fields = [
|
||||||
|
@ -663,7 +654,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard", count=4)
|
self.assertContains(response, "button--clipboard", count=3)
|
||||||
|
|
||||||
# cleanup this test
|
# cleanup this test
|
||||||
domain_info.delete()
|
domain_info.delete()
|
||||||
|
@ -687,7 +678,6 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
"more_organization_information",
|
"more_organization_information",
|
||||||
"domain",
|
"domain",
|
||||||
"domain_request",
|
"domain_request",
|
||||||
"submitter",
|
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
@ -706,19 +696,19 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
# Assert that sorting in reverse works correctly
|
# Assert that sorting in reverse works correctly
|
||||||
self.test_helper.assert_table_sorted("-1", ("-domain__name",))
|
self.test_helper.assert_table_sorted("-1", ("-domain__name",))
|
||||||
|
|
||||||
def test_submitter_sortable(self):
|
def test_creator_sortable(self):
|
||||||
"""Tests if DomainInformation sorts by submitter correctly"""
|
"""Tests if DomainInformation sorts by creator correctly"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
# Assert that our sort works correctly
|
# Assert that our sort works correctly
|
||||||
self.test_helper.assert_table_sorted(
|
self.test_helper.assert_table_sorted(
|
||||||
"4",
|
"4",
|
||||||
("submitter__first_name", "submitter__last_name"),
|
("creator__first_name", "creator__last_name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assert that sorting in reverse works correctly
|
# Assert that sorting in reverse works correctly
|
||||||
self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name"))
|
self.test_helper.assert_table_sorted("-4", ("-creator__first_name", "-creator__last_name"))
|
||||||
|
|
||||||
|
|
||||||
class TestUserDomainRoleAdmin(TestCase):
|
class TestUserDomainRoleAdmin(TestCase):
|
||||||
|
@ -973,7 +963,7 @@ class TestListHeaderAdmin(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestMyUserAdmin(MockDbForSharedTests):
|
class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
||||||
"""Tests for the MyUserAdmin class as super or staff user
|
"""Tests for the MyUserAdmin class as super or staff user
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
@ -993,6 +983,7 @@ class TestMyUserAdmin(MockDbForSharedTests):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.app.set_user(self.superuser.username)
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -1227,6 +1218,20 @@ class TestMyUserAdmin(MockDbForSharedTests):
|
||||||
self.assertNotContains(response, "Portfolio roles:")
|
self.assertNotContains(response, "Portfolio roles:")
|
||||||
self.assertNotContains(response, "Portfolio additional permissions:")
|
self.assertNotContains(response, "Portfolio additional permissions:")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_user_can_see_related_portfolios(self):
|
||||||
|
"""Tests if a user can see the portfolios they are associated with on the user page"""
|
||||||
|
portfolio, _ = Portfolio.objects.get_or_create(organization_name="test", creator=self.superuser)
|
||||||
|
permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.superuser, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
response = self.app.get(reverse("admin:registrar_user_change", args=[self.superuser.pk]))
|
||||||
|
expected_href = reverse("admin:registrar_portfolio_change", args=[portfolio.pk])
|
||||||
|
self.assertContains(response, expected_href)
|
||||||
|
self.assertContains(response, str(portfolio))
|
||||||
|
permission.delete()
|
||||||
|
portfolio.delete()
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdminTest(TestCase):
|
class AuditedAdminTest(TestCase):
|
||||||
|
|
||||||
|
@ -1301,7 +1306,6 @@ class AuditedAdminTest(TestCase):
|
||||||
# Senior offical is commented out for now - this is alphabetized
|
# Senior offical is commented out for now - this is alphabetized
|
||||||
# and this test does not accurately reflect that.
|
# and this test does not accurately reflect that.
|
||||||
# DomainRequest.senior_official.field,
|
# DomainRequest.senior_official.field,
|
||||||
DomainRequest.submitter.field,
|
|
||||||
# DomainRequest.investigator.field,
|
# DomainRequest.investigator.field,
|
||||||
DomainRequest.creator.field,
|
DomainRequest.creator.field,
|
||||||
DomainRequest.requested_domain.field,
|
DomainRequest.requested_domain.field,
|
||||||
|
@ -1361,7 +1365,6 @@ class AuditedAdminTest(TestCase):
|
||||||
# Senior offical is commented out for now - this is alphabetized
|
# Senior offical is commented out for now - this is alphabetized
|
||||||
# and this test does not accurately reflect that.
|
# and this test does not accurately reflect that.
|
||||||
# DomainInformation.senior_official.field,
|
# DomainInformation.senior_official.field,
|
||||||
DomainInformation.submitter.field,
|
|
||||||
# DomainInformation.creator.field,
|
# DomainInformation.creator.field,
|
||||||
(DomainInformation.domain.field, ["name"]),
|
(DomainInformation.domain.field, ["name"]),
|
||||||
(DomainInformation.domain_request.field, ["requested_domain__name"]),
|
(DomainInformation.domain_request.field, ["requested_domain__name"]),
|
||||||
|
@ -1670,91 +1673,6 @@ class TestContactAdmin(TestCase):
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
def test_change_view_for_joined_contact_five_or_less(self):
|
|
||||||
"""Create a contact, join it to 4 domain requests.
|
|
||||||
Assert that the warning on the contact form lists 4 joins."""
|
|
||||||
with less_console_noise():
|
|
||||||
self.client.force_login(self.superuser)
|
|
||||||
|
|
||||||
# Create an instance of the model
|
|
||||||
contact, _ = Contact.objects.get_or_create(
|
|
||||||
first_name="Henry",
|
|
||||||
last_name="McFakerson",
|
|
||||||
)
|
|
||||||
|
|
||||||
# join it to 4 domain requests.
|
|
||||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
|
||||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
|
||||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
|
||||||
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
|
|
||||||
|
|
||||||
with patch("django.contrib.messages.warning") as mock_warning:
|
|
||||||
# Use the test client to simulate the request
|
|
||||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
|
||||||
|
|
||||||
# Assert that the error message was called with the correct argument
|
|
||||||
# Note: The 5th join will be a user.
|
|
||||||
mock_warning.assert_called_once_with(
|
|
||||||
response.wsgi_request,
|
|
||||||
"<ul class='messagelist_content-list--unstyled'>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request1.pk}/change/'>city1.gov</a></li>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request2.pk}/change/'>city2.gov</a></li>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
|
||||||
"</ul>",
|
|
||||||
)
|
|
||||||
|
|
||||||
# cleanup this test
|
|
||||||
DomainRequest.objects.all().delete()
|
|
||||||
contact.delete()
|
|
||||||
|
|
||||||
def test_change_view_for_joined_contact_five_or_more(self):
|
|
||||||
"""Create a contact, join it to 6 domain requests.
|
|
||||||
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
|
||||||
with less_console_noise():
|
|
||||||
self.client.force_login(self.superuser)
|
|
||||||
# Create an instance of the model
|
|
||||||
# join it to 6 domain requests.
|
|
||||||
contact, _ = Contact.objects.get_or_create(
|
|
||||||
first_name="Henry",
|
|
||||||
last_name="McFakerson",
|
|
||||||
)
|
|
||||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
|
||||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
|
||||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
|
||||||
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
|
|
||||||
domain_request5 = completed_domain_request(submitter=contact, name="city5.gov")
|
|
||||||
completed_domain_request(submitter=contact, name="city6.gov")
|
|
||||||
with patch("django.contrib.messages.warning") as mock_warning:
|
|
||||||
# Use the test client to simulate the request
|
|
||||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
|
||||||
logger.debug(mock_warning)
|
|
||||||
# Assert that the error message was called with the correct argument
|
|
||||||
# Note: The 6th join will be a user.
|
|
||||||
mock_warning.assert_called_once_with(
|
|
||||||
response.wsgi_request,
|
|
||||||
"<ul class='messagelist_content-list--unstyled'>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request1.pk}/change/'>city1.gov</a></li>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request2.pk}/change/'>city2.gov</a></li>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
|
||||||
f"domainrequest/{domain_request5.pk}/change/'>city5.gov</a></li>"
|
|
||||||
"</ul>"
|
|
||||||
"<p class='font-sans-3xs'>And 1 more...</p>",
|
|
||||||
)
|
|
||||||
# cleanup this test
|
|
||||||
DomainRequest.objects.all().delete()
|
|
||||||
contact.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class TestVerifiedByStaffAdmin(TestCase):
|
class TestVerifiedByStaffAdmin(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -427,13 +427,6 @@ class TestDomainAdminWithClient(TestCase):
|
||||||
# Check for the field itself
|
# Check for the field itself
|
||||||
self.assertContains(response, "Meoward Jones")
|
self.assertContains(response, "Meoward Jones")
|
||||||
|
|
||||||
# == Check for the submitter == #
|
|
||||||
self.assertContains(response, "mayor@igorville.gov")
|
|
||||||
|
|
||||||
self.assertContains(response, "Admin Tester")
|
|
||||||
self.assertContains(response, "(555) 555 5556")
|
|
||||||
self.assertContains(response, "Testy2 Tester2")
|
|
||||||
|
|
||||||
# == Check for the senior_official == #
|
# == Check for the senior_official == #
|
||||||
self.assertContains(response, "testy@town.com")
|
self.assertContains(response, "testy@town.com")
|
||||||
self.assertContains(response, "Chief Tester")
|
self.assertContains(response, "Chief Tester")
|
||||||
|
|
|
@ -100,7 +100,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
||||||
|
|
||||||
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
||||||
domain_request = completed_domain_request(submitter=contact, name="city1.gov")
|
domain_request = completed_domain_request(name="city1.gov")
|
||||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||||
model_admin = AuditedAdmin(DomainRequest, self.site)
|
model_admin = AuditedAdmin(DomainRequest, self.site)
|
||||||
|
|
||||||
|
@ -155,11 +155,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
# These should exist in the response
|
# These should exist in the response
|
||||||
expected_values = [
|
expected_values = [
|
||||||
("creator", "Person who submitted the domain request; will not receive email updates"),
|
("creator", "Person who submitted the domain request. Will receive email updates"),
|
||||||
(
|
|
||||||
"submitter",
|
|
||||||
'Person listed under "your contact information" in the request form; will receive email updates',
|
|
||||||
),
|
|
||||||
("approved_domain", "Domain associated with this request; will be blank until request is approved"),
|
("approved_domain", "Domain associated with this request; will be blank until request is approved"),
|
||||||
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
||||||
("alternative_domains", "Other domain names the creator provided for consideration"),
|
("alternative_domains", "Other domain names the creator provided for consideration"),
|
||||||
|
@ -442,8 +438,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",))
|
self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",))
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_submitter_sortable(self):
|
def test_creator_sortable(self):
|
||||||
"""Tests if the DomainRequest sorts by submitter correctly"""
|
"""Tests if the DomainRequest sorts by creator correctly"""
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
multiple_unalphabetical_domain_objects("domain_request")
|
multiple_unalphabetical_domain_objects("domain_request")
|
||||||
|
@ -457,8 +453,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.test_helper.assert_table_sorted(
|
self.test_helper.assert_table_sorted(
|
||||||
"13",
|
"13",
|
||||||
(
|
(
|
||||||
"submitter__first_name",
|
"creator__first_name",
|
||||||
"submitter__last_name",
|
"creator__last_name",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -466,8 +462,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.test_helper.assert_table_sorted(
|
self.test_helper.assert_table_sorted(
|
||||||
"-13",
|
"-13",
|
||||||
(
|
(
|
||||||
"-submitter__first_name",
|
"-creator__first_name",
|
||||||
"-submitter__last_name",
|
"-creator__last_name",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -665,15 +661,24 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
def test_action_needed_sends_reason_email_prod_bcc(self):
|
def test_action_needed_sends_reason_email_prod_bcc(self):
|
||||||
"""When an action needed reason is set, an email is sent out and help@get.gov
|
"""When an action needed reason is set, an email is sent out and help@get.gov
|
||||||
is BCC'd in production"""
|
is BCC'd in production"""
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "meoward.jones@igorville.gov"
|
||||||
|
|
||||||
|
_creator = User.objects.create(
|
||||||
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email=EMAIL,
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||||
User.objects.filter(email=EMAIL).delete()
|
|
||||||
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||||
action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request
|
||||||
domain_request = completed_domain_request(status=in_review)
|
domain_request = completed_domain_request(status=in_review, user=_creator)
|
||||||
|
|
||||||
# Test the email sent out for already_has_domains
|
# Test the email sent out for already_has_domains
|
||||||
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
||||||
|
@ -702,7 +707,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
|
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL
|
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, _creator.email, bcc_email_address=BCC_EMAIL
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||||
|
|
||||||
|
@ -723,7 +728,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
)
|
)
|
||||||
|
|
||||||
domain_request.refresh_from_db()
|
domain_request.refresh_from_db()
|
||||||
self.assert_email_is_accurate("custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL)
|
self.assert_email_is_accurate("custom email content", 4, _creator.email, bcc_email_address=BCC_EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||||
|
|
||||||
# Tests if a new email gets sent when just the email is changed.
|
# Tests if a new email gets sent when just the email is changed.
|
||||||
|
@ -747,7 +752,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
action_needed_reason=eligibility_unclear,
|
action_needed_reason=eligibility_unclear,
|
||||||
action_needed_reason_email="custom content when starting anew",
|
action_needed_reason_email="custom content when starting anew",
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate("custom content when starting anew", 5, EMAIL, bcc_email_address=BCC_EMAIL)
|
self.assert_email_is_accurate(
|
||||||
|
"custom content when starting anew", 5, _creator.email, bcc_email_address=BCC_EMAIL
|
||||||
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
||||||
|
|
||||||
# def test_action_needed_sends_reason_email_prod_bcc(self):
|
# def test_action_needed_sends_reason_email_prod_bcc(self):
|
||||||
|
@ -811,22 +818,31 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
Also test that the default email set in settings is NOT BCCd on non-prod whenever
|
Also test that the default email set in settings is NOT BCCd on non-prod whenever
|
||||||
an email does go out."""
|
an email does go out."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
EMAIL = "meoward.jones@igorville.gov"
|
||||||
EMAIL = "mayor@igorville.gov"
|
|
||||||
User.objects.filter(email=EMAIL).delete()
|
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create fake creator
|
||||||
domain_request = completed_domain_request()
|
_creator = User.objects.create(
|
||||||
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email=EMAIL,
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a sample domain request and whitelist user email
|
||||||
|
domain_request = completed_domain_request(user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Test Submitted Status from started
|
# Test Submitted Status from started
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True)
|
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, True)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Test Withdrawn Status
|
# Test Withdrawn Status
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL, True
|
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, _creator.email, True
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
@ -885,30 +901,37 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
Also test that the default email set in settings IS BCCd on prod whenever
|
Also test that the default email set in settings IS BCCd on prod whenever
|
||||||
an email does go out."""
|
an email does go out."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request()
|
domain_request = completed_domain_request(user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Test Submitted Status from started
|
# Test Submitted Status from started
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, False, BCC_EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Test Withdrawn Status
|
# Test Withdrawn Status
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
|
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, _creator.email
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
# Test Submitted Status Again (from withdrawn)
|
# Test Submitted Status Again (from withdrawn)
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, False, BCC_EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
# Move it to IN_REVIEW
|
# Move it to IN_REVIEW
|
||||||
|
@ -938,16 +961,23 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"""When transitioning to approved on a domain request,
|
"""When transitioning to approved on a domain request,
|
||||||
an email is sent out every time."""
|
an email is sent out every time."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Test Submitted Status
|
# Test Submitted Status
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Test Withdrawn Status
|
# Test Withdrawn Status
|
||||||
|
@ -956,7 +986,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.DOMAIN_PURPOSE,
|
DomainRequest.RejectionReasons.DOMAIN_PURPOSE,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
|
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
# Test Submitted Status Again (No new email should be sent)
|
# Test Submitted Status Again (No new email should be sent)
|
||||||
|
@ -968,12 +998,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"""When transitioning to rejected on a domain request, an email is sent
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
explaining why when the reason is domain purpose."""
|
explaining why when the reason is domain purpose."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Reject for reason DOMAIN_PURPOSE and test email
|
# Reject for reason DOMAIN_PURPOSE and test email
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
|
@ -984,13 +1021,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because the purpose you provided did not meet our \nrequirements.",
|
"Your domain request was rejected because the purpose you provided did not meet our \nrequirements.",
|
||||||
0,
|
0,
|
||||||
EMAIL,
|
_creator.email,
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Approve
|
# Approve
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -998,12 +1035,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"""When transitioning to rejected on a domain request, an email is sent
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
explaining why when the reason is requestor."""
|
explaining why when the reason is requestor."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Reject for reason REQUESTOR and test email including dynamic organization name
|
# Reject for reason REQUESTOR and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
|
@ -1013,13 +1057,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
||||||
"domain on behalf of Testorg",
|
"domain on behalf of Testorg",
|
||||||
0,
|
0,
|
||||||
EMAIL,
|
_creator.email,
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Approve
|
# Approve
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -1027,12 +1071,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"""When transitioning to rejected on a domain request, an email is sent
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
explaining why when the reason is second domain."""
|
explaining why when the reason is second domain."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name
|
# Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
|
@ -1040,25 +1091,35 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING,
|
DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL)
|
self.assert_email_is_accurate(
|
||||||
|
"Your domain request was rejected because Testorg has a .gov domain.", 0, _creator.email
|
||||||
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Approve
|
# Approve
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self):
|
def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self):
|
||||||
"""When transitioning to rejected on a domain request, an email is sent
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
explaining why when the reason is contacts or org legitimacy."""
|
explaining why when the reason is contacts or org legitimacy."""
|
||||||
|
# Create fake creator
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
EMAIL = "meoward.jones@igorville.gov"
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email=EMAIL,
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name
|
# Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
|
@ -1070,13 +1131,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"Your domain request was rejected because we could not verify the organizational \n"
|
"Your domain request was rejected because we could not verify the organizational \n"
|
||||||
"contacts you provided. If you have questions or comments, reply to this email.",
|
"contacts you provided. If you have questions or comments, reply to this email.",
|
||||||
0,
|
0,
|
||||||
EMAIL,
|
_creator.email,
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Approve
|
# Approve
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -1084,12 +1145,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"""When transitioning to rejected on a domain request, an email is sent
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
explaining why when the reason is org eligibility."""
|
explaining why when the reason is org eligibility."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name
|
# Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
|
@ -1101,26 +1169,32 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
||||||
"a .gov domain.",
|
"a .gov domain.",
|
||||||
0,
|
0,
|
||||||
EMAIL,
|
_creator.email,
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Approve
|
# Approve
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_save_model_sends_rejected_email_naming(self):
|
def test_save_model_sends_rejected_email_naming(self):
|
||||||
"""When transitioning to rejected on a domain request, an email is sent
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
explaining why when the reason is naming."""
|
explaining why when the reason is naming."""
|
||||||
|
# Create fake creator
|
||||||
|
_creator = User.objects.create(
|
||||||
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create a sample domain request and whitelist user email
|
||||||
EMAIL = "mayor@igorville.gov"
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
User.objects.filter(email=EMAIL).delete()
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Create a sample domain request
|
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
|
||||||
|
|
||||||
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
|
@ -1129,13 +1203,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
DomainRequest.RejectionReasons.NAMING_REQUIREMENTS,
|
DomainRequest.RejectionReasons.NAMING_REQUIREMENTS,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL
|
"Your domain request was rejected because it does not meet our naming requirements.", 0, _creator.email
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Approve
|
# Approve
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -1143,12 +1217,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"""When transitioning to rejected on a domain request, an email is sent
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
explaining why when the reason is other."""
|
explaining why when the reason is other."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelist user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
|
@ -1156,12 +1237,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.OTHER,
|
DomainRequest.RejectionReasons.OTHER,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL)
|
self.assert_email_is_accurate("Choosing a .gov domain name", 0, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Approve
|
# Approve
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -1225,23 +1306,30 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"""When transitioning to withdrawn on a domain request,
|
"""When transitioning to withdrawn on a domain request,
|
||||||
an email is sent out every time."""
|
an email is sent out every time."""
|
||||||
|
|
||||||
# Ensure there is no user with this email
|
# Create fake creator
|
||||||
EMAIL = "mayor@igorville.gov"
|
_creator = User.objects.create(
|
||||||
User.objects.filter(email=EMAIL).delete()
|
username="MrMeoward",
|
||||||
|
first_name="Meoward",
|
||||||
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# Create a sample domain request
|
# Create a sample domain request and whitelists user email
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||||
|
|
||||||
# Test Submitted Status
|
# Test Submitted Status
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL
|
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, _creator.email
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Test Withdrawn Status
|
# Test Withdrawn Status
|
||||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||||
self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL)
|
self.assert_email_is_accurate("We received your .gov domain request.", 1, _creator.email)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
# Test Submitted Status Again (No new email should be sent)
|
# Test Submitted Status Again (No new email should be sent)
|
||||||
|
@ -1420,16 +1508,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Check for the field itself
|
# Check for the field itself
|
||||||
self.assertContains(response, "Meoward Jones")
|
self.assertContains(response, "Meoward Jones")
|
||||||
|
|
||||||
# == Check for the submitter == #
|
|
||||||
self.assertContains(response, "mayor@igorville.gov", count=2)
|
|
||||||
expected_submitter_fields = [
|
|
||||||
# Field, expected value
|
|
||||||
("title", "Admin Tester"),
|
|
||||||
("phone", "(555) 555 5556"),
|
|
||||||
]
|
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
|
|
||||||
self.assertContains(response, "Testy2 Tester2")
|
|
||||||
|
|
||||||
# == Check for the senior_official == #
|
# == Check for the senior_official == #
|
||||||
self.assertContains(response, "testy@town.com", count=2)
|
self.assertContains(response, "testy@town.com", count=2)
|
||||||
expected_so_fields = [
|
expected_so_fields = [
|
||||||
|
@ -1450,7 +1528,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard", count=5)
|
self.assertContains(response, "button--clipboard", count=4)
|
||||||
|
|
||||||
# Test that Creator counts display properly
|
# Test that Creator counts display properly
|
||||||
self.assertNotContains(response, "Approved domains")
|
self.assertNotContains(response, "Approved domains")
|
||||||
|
@ -1585,7 +1663,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"senior_official",
|
"senior_official",
|
||||||
"approved_domain",
|
"approved_domain",
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
"submitter",
|
|
||||||
"purpose",
|
"purpose",
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
|
@ -1624,7 +1701,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"approved_domain",
|
"approved_domain",
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"purpose",
|
"purpose",
|
||||||
"submitter",
|
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
|
|
@ -7,7 +7,7 @@ from waffle.testutils import override_flag
|
||||||
from registrar.utility import email
|
from registrar.utility import email
|
||||||
from registrar.utility.email import send_templated_email
|
from registrar.utility.email import send_templated_email
|
||||||
from .common import completed_domain_request
|
from .common import completed_domain_request
|
||||||
from registrar.models import AllowedEmail
|
from registrar.models import AllowedEmail, User
|
||||||
|
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -109,7 +109,6 @@ class TestEmails(TestCase):
|
||||||
|
|
||||||
# check for optional things
|
# check for optional things
|
||||||
self.assertIn("Other employees from your organization:", body)
|
self.assertIn("Other employees from your organization:", body)
|
||||||
self.assertIn("Testy2 Tester2", body)
|
|
||||||
self.assertIn("Current websites:", body)
|
self.assertIn("Current websites:", body)
|
||||||
self.assertIn("city.com", body)
|
self.assertIn("city.com", body)
|
||||||
self.assertIn("About your organization:", body)
|
self.assertIn("About your organization:", body)
|
||||||
|
@ -146,14 +145,20 @@ class TestEmails(TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_submission_confirmation_other_contacts_spacing(self):
|
def test_submission_confirmation_other_contacts_spacing(self):
|
||||||
"""Test line spacing with other contacts."""
|
"""Test line spacing with other contacts."""
|
||||||
domain_request = completed_domain_request(has_other_contacts=True)
|
|
||||||
|
# Create fake creator
|
||||||
|
_creator = User.objects.create(
|
||||||
|
username="MrMeoward", first_name="Meoward", last_name="Jones", phone="(888) 888 8888"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a fake domain request
|
||||||
|
domain_request = completed_domain_request(has_other_contacts=True, user=_creator)
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||||
domain_request.submit()
|
domain_request.submit()
|
||||||
_, kwargs = self.mock_client.send_email.call_args
|
_, kwargs = self.mock_client.send_email.call_args
|
||||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("Other employees from your organization:", body)
|
self.assertIn("Other employees from your organization:", body)
|
||||||
# spacing should be right between adjacent elements
|
self.assertRegex(body, r"8888\n\nOther employees")
|
||||||
self.assertRegex(body, r"5556\n\nOther employees")
|
|
||||||
self.assertRegex(body, r"5557\n\nAnything else")
|
self.assertRegex(body, r"5557\n\nAnything else")
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
|
@ -166,7 +171,6 @@ class TestEmails(TestCase):
|
||||||
_, kwargs = self.mock_client.send_email.call_args
|
_, kwargs = self.mock_client.send_email.call_args
|
||||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
# spacing should be right between adjacent elements
|
# spacing should be right between adjacent elements
|
||||||
self.assertRegex(body, r"5556\n\nOther employees")
|
|
||||||
self.assertRegex(body, r"None\n\nAnything else")
|
self.assertRegex(body, r"None\n\nAnything else")
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
|
|
|
@ -10,7 +10,6 @@ from registrar.forms.domain_request_wizard import (
|
||||||
DotGovDomainForm,
|
DotGovDomainForm,
|
||||||
SeniorOfficialForm,
|
SeniorOfficialForm,
|
||||||
OrganizationContactForm,
|
OrganizationContactForm,
|
||||||
YourContactForm,
|
|
||||||
OtherContactsForm,
|
OtherContactsForm,
|
||||||
RequirementsForm,
|
RequirementsForm,
|
||||||
TribalGovernmentForm,
|
TribalGovernmentForm,
|
||||||
|
@ -366,19 +365,6 @@ class TestFormValidation(MockEppLib):
|
||||||
["Response must be less than 2000 characters."],
|
["Response must be less than 2000 characters."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_your_contact_email_invalid(self):
|
|
||||||
"""must be a valid email address."""
|
|
||||||
form = YourContactForm(data={"email": "boss@boss"})
|
|
||||||
self.assertEqual(
|
|
||||||
form.errors["email"],
|
|
||||||
["Enter your email address in the required format, like name@example.com."],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_your_contact_phone_invalid(self):
|
|
||||||
"""Must be a valid phone number."""
|
|
||||||
form = YourContactForm(data={"phone": "boss@boss"})
|
|
||||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
|
|
||||||
|
|
||||||
def test_other_contact_email_invalid(self):
|
def test_other_contact_email_invalid(self):
|
||||||
"""must be a valid email address."""
|
"""must be a valid email address."""
|
||||||
form = OtherContactsForm(data={"email": "splendid@boss"})
|
form = OtherContactsForm(data={"email": "splendid@boss"})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import copy
|
import copy
|
||||||
|
import boto3_mocking # type: ignore
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
@ -8,6 +9,7 @@ from django.utils import timezone
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
import logging
|
import logging
|
||||||
import pyzipper
|
import pyzipper
|
||||||
|
from django.core.management.base import CommandError
|
||||||
from registrar.management.commands.clean_tables import Command as CleanTablesCommand
|
from registrar.management.commands.clean_tables import Command as CleanTablesCommand
|
||||||
from registrar.management.commands.export_tables import Command as ExportTablesCommand
|
from registrar.management.commands.export_tables import Command as ExportTablesCommand
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
|
@ -23,14 +25,17 @@ from registrar.models import (
|
||||||
VerifiedByStaff,
|
VerifiedByStaff,
|
||||||
PublicContact,
|
PublicContact,
|
||||||
FederalAgency,
|
FederalAgency,
|
||||||
|
Portfolio,
|
||||||
|
Suborganization,
|
||||||
)
|
)
|
||||||
import tablib
|
import tablib
|
||||||
from unittest.mock import patch, call, MagicMock, mock_open
|
from unittest.mock import patch, call, MagicMock, mock_open
|
||||||
from epplibwrapper import commands, common
|
from epplibwrapper import commands, common
|
||||||
|
|
||||||
from .common import MockEppLib, less_console_noise, completed_domain_request
|
from .common import MockEppLib, less_console_noise, completed_domain_request, MockSESClient
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1408,3 +1413,137 @@ class TestPopulateFederalAgencyInitialsAndFceb(TestCase):
|
||||||
missing_agency.refresh_from_db()
|
missing_agency.refresh_from_db()
|
||||||
self.assertIsNone(missing_agency.initials)
|
self.assertIsNone(missing_agency.initials)
|
||||||
self.assertIsNone(missing_agency.is_fceb)
|
self.assertIsNone(missing_agency.is_fceb)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateFederalPortfolio(TestCase):
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def setUp(self):
|
||||||
|
self.mock_client = MockSESClient()
|
||||||
|
self.user = User.objects.create(username="testuser")
|
||||||
|
self.federal_agency = FederalAgency.objects.create(agency="Test Federal Agency")
|
||||||
|
self.senior_official = SeniorOfficial.objects.create(
|
||||||
|
first_name="first", last_name="last", email="testuser@igorville.gov", federal_agency=self.federal_agency
|
||||||
|
)
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
self.domain_request = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||||
|
federal_agency=self.federal_agency,
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
self.domain_request.approve()
|
||||||
|
self.domain_info = DomainInformation.objects.filter(domain_request=self.domain_request).get()
|
||||||
|
|
||||||
|
self.domain_request_2 = completed_domain_request(
|
||||||
|
name="sock@igorville.org",
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||||
|
federal_agency=self.federal_agency,
|
||||||
|
user=self.user,
|
||||||
|
organization_name="Test Federal Agency",
|
||||||
|
)
|
||||||
|
self.domain_request_2.approve()
|
||||||
|
self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
Suborganization.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
SeniorOfficial.objects.all().delete()
|
||||||
|
FederalAgency.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def run_create_federal_portfolio(self, agency_name, parse_requests=False, parse_domains=False):
|
||||||
|
with patch(
|
||||||
|
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
call_command(
|
||||||
|
"create_federal_portfolio", agency_name, parse_requests=parse_requests, parse_domains=parse_domains
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_or_modify_portfolio(self):
|
||||||
|
"""Test portfolio creation and modification with suborg and senior official."""
|
||||||
|
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||||
|
|
||||||
|
portfolio = Portfolio.objects.get(federal_agency=self.federal_agency)
|
||||||
|
self.assertEqual(portfolio.organization_name, self.federal_agency.agency)
|
||||||
|
self.assertEqual(portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||||
|
self.assertEqual(portfolio.creator, User.get_default_user())
|
||||||
|
self.assertEqual(portfolio.notes, "Auto-generated record")
|
||||||
|
|
||||||
|
# Test the suborgs
|
||||||
|
suborganizations = Suborganization.objects.filter(portfolio__federal_agency=self.federal_agency)
|
||||||
|
self.assertEqual(suborganizations.count(), 1)
|
||||||
|
self.assertEqual(suborganizations.first().name, "Testorg")
|
||||||
|
|
||||||
|
# Test the senior official
|
||||||
|
self.assertEqual(portfolio.senior_official, self.senior_official)
|
||||||
|
|
||||||
|
def test_handle_portfolio_requests(self):
|
||||||
|
"""Verify portfolio association with domain requests."""
|
||||||
|
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||||
|
|
||||||
|
self.domain_request.refresh_from_db()
|
||||||
|
self.assertIsNotNone(self.domain_request.portfolio)
|
||||||
|
self.assertEqual(self.domain_request.portfolio.federal_agency, self.federal_agency)
|
||||||
|
self.assertEqual(self.domain_request.sub_organization.name, "Testorg")
|
||||||
|
|
||||||
|
def test_handle_portfolio_domains(self):
|
||||||
|
"""Check portfolio association with domain information."""
|
||||||
|
self.run_create_federal_portfolio("Test Federal Agency", parse_domains=True)
|
||||||
|
|
||||||
|
self.domain_info.refresh_from_db()
|
||||||
|
self.assertIsNotNone(self.domain_info.portfolio)
|
||||||
|
self.assertEqual(self.domain_info.portfolio.federal_agency, self.federal_agency)
|
||||||
|
self.assertEqual(self.domain_info.sub_organization.name, "Testorg")
|
||||||
|
|
||||||
|
def test_handle_parse_both(self):
|
||||||
|
"""Ensure correct parsing of both requests and domains."""
|
||||||
|
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True, parse_domains=True)
|
||||||
|
|
||||||
|
self.domain_request.refresh_from_db()
|
||||||
|
self.domain_info.refresh_from_db()
|
||||||
|
self.assertIsNotNone(self.domain_request.portfolio)
|
||||||
|
self.assertIsNotNone(self.domain_info.portfolio)
|
||||||
|
self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio)
|
||||||
|
|
||||||
|
def test_command_error_no_parse_options(self):
|
||||||
|
"""Verify error when no parse options are provided."""
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
CommandError, "You must specify at least one of --parse_requests or --parse_domains."
|
||||||
|
):
|
||||||
|
self.run_create_federal_portfolio("Test Federal Agency")
|
||||||
|
|
||||||
|
def test_command_error_agency_not_found(self):
|
||||||
|
"""Check error handling for non-existent agency."""
|
||||||
|
expected_message = (
|
||||||
|
"Cannot find the federal agency 'Non-existent Agency' in our database. "
|
||||||
|
"The value you enter for `agency_name` must be prepopulated in the FederalAgency table before proceeding."
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, expected_message):
|
||||||
|
self.run_create_federal_portfolio("Non-existent Agency", parse_requests=True)
|
||||||
|
|
||||||
|
def test_update_existing_portfolio(self):
|
||||||
|
"""Test updating an existing portfolio."""
|
||||||
|
# Create an existing portfolio
|
||||||
|
existing_portfolio = Portfolio.objects.create(
|
||||||
|
federal_agency=self.federal_agency,
|
||||||
|
organization_name="Test Federal Agency",
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.CITY,
|
||||||
|
creator=self.user,
|
||||||
|
notes="Old notes",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||||
|
|
||||||
|
existing_portfolio.refresh_from_db()
|
||||||
|
self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency)
|
||||||
|
self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||||
|
|
||||||
|
# Notes and creator should be untouched
|
||||||
|
self.assertEqual(existing_portfolio.notes, "Old notes")
|
||||||
|
self.assertEqual(existing_portfolio.creator, self.user)
|
||||||
|
|
|
@ -170,7 +170,6 @@ class TestDomainRequest(TestCase):
|
||||||
zipcode="12345-6789",
|
zipcode="12345-6789",
|
||||||
senior_official=contact,
|
senior_official=contact,
|
||||||
requested_domain=domain,
|
requested_domain=domain,
|
||||||
submitter=contact,
|
|
||||||
purpose="Igorville rules!",
|
purpose="Igorville rules!",
|
||||||
anything_else="All of Igorville loves the dotgov program.",
|
anything_else="All of Igorville loves the dotgov program.",
|
||||||
is_policy_acknowledged=True,
|
is_policy_acknowledged=True,
|
||||||
|
@ -197,7 +196,6 @@ class TestDomainRequest(TestCase):
|
||||||
state_territory="CA",
|
state_territory="CA",
|
||||||
zipcode="12345-6789",
|
zipcode="12345-6789",
|
||||||
senior_official=contact,
|
senior_official=contact,
|
||||||
submitter=contact,
|
|
||||||
purpose="Igorville rules!",
|
purpose="Igorville rules!",
|
||||||
anything_else="All of Igorville loves the dotgov program.",
|
anything_else="All of Igorville loves the dotgov program.",
|
||||||
is_policy_acknowledged=True,
|
is_policy_acknowledged=True,
|
||||||
|
@ -225,7 +223,7 @@ class TestDomainRequest(TestCase):
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
domain_request = DomainRequest.objects.create(creator=user, requested_domain=site)
|
domain_request = DomainRequest.objects.create(creator=user, requested_domain=site)
|
||||||
|
|
||||||
# no submitter email so this emits a log warning
|
# no email sent to creator so this emits a log warning
|
||||||
|
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
|
@ -258,19 +256,11 @@ class TestDomainRequest(TestCase):
|
||||||
|
|
||||||
email_allowed.delete()
|
email_allowed.delete()
|
||||||
|
|
||||||
@override_flag("profile_feature", active=False)
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_submit_from_started_sends_email(self):
|
|
||||||
msg = "Create a domain request and submit it and see if email was sent."
|
|
||||||
domain_request = completed_domain_request(submitter=self.dummy_user, user=self.dummy_user_2)
|
|
||||||
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello")
|
|
||||||
|
|
||||||
@override_flag("profile_feature", active=True)
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_submit_from_started_sends_email_to_creator(self):
|
def test_submit_from_started_sends_email_to_creator(self):
|
||||||
"""Tests if, when the profile feature flag is on, we send an email to the creator"""
|
"""tests that we send an email to the creator"""
|
||||||
msg = "Create a domain request and submit it and see if email was sent when the feature flag is on."
|
msg = "Create a domain request and submit it and see if email was sent when the feature flag is on."
|
||||||
domain_request = completed_domain_request(submitter=self.dummy_user, user=self.dummy_user_2)
|
domain_request = completed_domain_request(user=self.dummy_user_2)
|
||||||
self.check_email_sent(
|
self.check_email_sent(
|
||||||
domain_request, msg, "submit", 1, expected_content="Lava", expected_email="intern@igorville.com"
|
domain_request, msg, "submit", 1, expected_content="Lava", expected_email="intern@igorville.com"
|
||||||
)
|
)
|
||||||
|
@ -278,10 +268,9 @@ class TestDomainRequest(TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_submit_from_withdrawn_sends_email(self):
|
def test_submit_from_withdrawn_sends_email(self):
|
||||||
msg = "Create a withdrawn domain request and submit it and see if email was sent."
|
msg = "Create a withdrawn domain request and submit it and see if email was sent."
|
||||||
domain_request = completed_domain_request(
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN, submitter=self.dummy_user
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=user)
|
||||||
)
|
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hi", expected_email=user.email)
|
||||||
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_submit_from_action_needed_does_not_send_email(self):
|
def test_submit_from_action_needed_does_not_send_email(self):
|
||||||
|
@ -298,26 +287,25 @@ class TestDomainRequest(TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_approve_sends_email(self):
|
def test_approve_sends_email(self):
|
||||||
msg = "Create a domain request and approve it and see if email was sent."
|
msg = "Create a domain request and approve it and see if email was sent."
|
||||||
domain_request = completed_domain_request(
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, submitter=self.dummy_user
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user)
|
||||||
)
|
self.check_email_sent(domain_request, msg, "approve", 1, expected_content="approved", expected_email=user.email)
|
||||||
self.check_email_sent(domain_request, msg, "approve", 1, expected_content="Hello")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_withdraw_sends_email(self):
|
def test_withdraw_sends_email(self):
|
||||||
msg = "Create a domain request and withdraw it and see if email was sent."
|
msg = "Create a domain request and withdraw it and see if email was sent."
|
||||||
domain_request = completed_domain_request(
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, submitter=self.dummy_user
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user)
|
||||||
|
self.check_email_sent(
|
||||||
|
domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email
|
||||||
)
|
)
|
||||||
self.check_email_sent(domain_request, msg, "withdraw", 1, expected_content="Hello")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_reject_sends_email(self):
|
def test_reject_sends_email(self):
|
||||||
msg = "Create a domain request and reject it and see if email was sent."
|
msg = "Create a domain request and reject it and see if email was sent."
|
||||||
domain_request = completed_domain_request(
|
user, _ = User.objects.get_or_create(username="testy")
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED, submitter=self.dummy_user
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user)
|
||||||
)
|
self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hi", expected_email=user.email)
|
||||||
self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hello")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_reject_with_prejudice_does_not_send_email(self):
|
def test_reject_with_prejudice_does_not_send_email(self):
|
||||||
|
@ -1135,7 +1123,7 @@ class TestPortfolioInvitations(TestCase):
|
||||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California")
|
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California")
|
||||||
self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||||
self.portfolio_role_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
self.portfolio_role_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||||
self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS
|
self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||||
self.portfolio_permission_2 = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
self.portfolio_permission_2 = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
||||||
self.invitation, _ = PortfolioInvitation.objects.get_or_create(
|
self.invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||||
email=self.email,
|
email=self.email,
|
||||||
|
@ -1326,16 +1314,16 @@ class TestUser(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
@patch.object(User, "has_edit_suborganization", return_value=True)
|
@patch.object(User, "has_edit_suborganization_portfolio_permission", return_value=True)
|
||||||
def test_portfolio_role_summary_admin(self, mock_edit_suborganization):
|
def test_portfolio_role_summary_admin(self, mock_edit_suborganization):
|
||||||
# Test if the user is recognized as an Admin
|
# Test if the user is recognized as an Admin
|
||||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"])
|
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"])
|
||||||
|
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_view_all_domains_permission=lambda self, portfolio: True,
|
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
|
has_any_requests_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_edit_requests=lambda self, portfolio: True,
|
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self):
|
def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self):
|
||||||
# Test if the user has both 'View-only admin' and 'Domain requestor' roles
|
# Test if the user has both 'View-only admin' and 'Domain requestor' roles
|
||||||
|
@ -1343,8 +1331,8 @@ class TestUser(TestCase):
|
||||||
|
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_view_all_domains_permission=lambda self, portfolio: True,
|
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
|
has_any_requests_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_view_only_admin(self):
|
def test_portfolio_role_summary_view_only_admin(self):
|
||||||
# Test if the user is recognized as a View-only admin
|
# Test if the user is recognized as a View-only admin
|
||||||
|
@ -1353,15 +1341,17 @@ class TestUser(TestCase):
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_base_portfolio_permission=lambda self, portfolio: True,
|
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_edit_requests=lambda self, portfolio: True,
|
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domains_portfolio_permission=lambda self, portfolio: True,
|
has_any_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_member_domain_requestor_domain_manager(self):
|
def test_portfolio_role_summary_member_domain_requestor_domain_manager(self):
|
||||||
# Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles
|
# Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles
|
||||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"])
|
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"])
|
||||||
|
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User, has_base_portfolio_permission=lambda self, portfolio: True, has_edit_requests=lambda self, portfolio: True
|
User,
|
||||||
|
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||||
|
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_member_domain_requestor(self):
|
def test_portfolio_role_summary_member_domain_requestor(self):
|
||||||
# Test if the user has 'Member' and 'Domain requestor' roles
|
# Test if the user has 'Member' and 'Domain requestor' roles
|
||||||
|
@ -1370,7 +1360,7 @@ class TestUser(TestCase):
|
||||||
@patch.multiple(
|
@patch.multiple(
|
||||||
User,
|
User,
|
||||||
has_base_portfolio_permission=lambda self, portfolio: True,
|
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||||
has_domains_portfolio_permission=lambda self, portfolio: True,
|
has_any_domains_portfolio_permission=lambda self, portfolio: True,
|
||||||
)
|
)
|
||||||
def test_portfolio_role_summary_member_domain_manager(self):
|
def test_portfolio_role_summary_member_domain_manager(self):
|
||||||
# Test if the user has 'Member' and 'Domain manager' roles
|
# Test if the user has 'Member' and 'Domain manager' roles
|
||||||
|
@ -1385,6 +1375,74 @@ class TestUser(TestCase):
|
||||||
# Test if the user has no roles
|
# Test if the user has no roles
|
||||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), [])
|
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), [])
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_base_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_base_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_edit_org_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_edit_org_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_any_domains_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.side_effect = [False, True] # First permission false, second permission true
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_any_domains_portfolio_permission(self.portfolio))
|
||||||
|
self.assertEqual(mock_has_permission.call_count, 2)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_view_all_domains_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_view_all_domains_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_has_any_requests_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.side_effect = [False, True] # First permission false, second permission true
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_any_requests_portfolio_permission(self.portfolio))
|
||||||
|
self.assertEqual(mock_has_permission.call_count, 2)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_view_all_requests_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_view_all_requests_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_edit_request_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_edit_request_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_view_suborganization_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_view_suborganization_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
||||||
|
|
||||||
|
@patch("registrar.models.User._has_portfolio_permission")
|
||||||
|
def test_has_edit_suborganization_portfolio_permission(self, mock_has_permission):
|
||||||
|
mock_has_permission.return_value = True
|
||||||
|
|
||||||
|
self.assertTrue(self.user.has_edit_suborganization_portfolio_permission(self.portfolio))
|
||||||
|
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_check_transition_domains_without_domains_on_login(self):
|
def test_check_transition_domains_without_domains_on_login(self):
|
||||||
"""A user's on_each_login callback does not check transition domains.
|
"""A user's on_each_login callback does not check transition domains.
|
||||||
|
@ -1547,8 +1605,8 @@ class TestUser(TestCase):
|
||||||
|
|
||||||
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertFalse(user_can_view_all_domains)
|
self.assertFalse(user_can_view_all_domains)
|
||||||
self.assertFalse(user_can_view_all_requests)
|
self.assertFalse(user_can_view_all_requests)
|
||||||
|
@ -1562,8 +1620,8 @@ class TestUser(TestCase):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertTrue(user_can_view_all_domains)
|
self.assertTrue(user_can_view_all_domains)
|
||||||
self.assertFalse(user_can_view_all_requests)
|
self.assertFalse(user_can_view_all_requests)
|
||||||
|
@ -1572,16 +1630,16 @@ class TestUser(TestCase):
|
||||||
portfolio_permission.save()
|
portfolio_permission.save()
|
||||||
portfolio_permission.refresh_from_db()
|
portfolio_permission.refresh_from_db()
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertTrue(user_can_view_all_domains)
|
self.assertTrue(user_can_view_all_domains)
|
||||||
self.assertTrue(user_can_view_all_requests)
|
self.assertTrue(user_can_view_all_requests)
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||||
|
|
||||||
self.assertTrue(user_can_view_all_domains)
|
self.assertTrue(user_can_view_all_domains)
|
||||||
self.assertTrue(user_can_view_all_requests)
|
self.assertTrue(user_can_view_all_requests)
|
||||||
|
@ -2069,7 +2127,6 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
requested_domain=draft_domain,
|
requested_domain=draft_domain,
|
||||||
purpose="Some purpose",
|
purpose="Some purpose",
|
||||||
submitter=you,
|
|
||||||
no_other_contacts_rationale=None,
|
no_other_contacts_rationale=None,
|
||||||
has_cisa_representative=True,
|
has_cisa_representative=True,
|
||||||
cisa_representative_email="somerep@cisa.com",
|
cisa_representative_email="somerep@cisa.com",
|
||||||
|
@ -2198,13 +2255,6 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
self.domain_request.save()
|
self.domain_request.save()
|
||||||
self.assertFalse(self.domain_request._is_purpose_complete())
|
self.assertFalse(self.domain_request._is_purpose_complete())
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_is_submitter_complete(self):
|
|
||||||
self.assertTrue(self.domain_request._is_submitter_complete())
|
|
||||||
self.domain_request.submitter = None
|
|
||||||
self.domain_request.save()
|
|
||||||
self.assertFalse(self.domain_request._is_submitter_complete())
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_is_other_contacts_complete_missing_one_field(self):
|
def test_is_other_contacts_complete_missing_one_field(self):
|
||||||
self.assertTrue(self.domain_request._is_other_contacts_complete())
|
self.assertTrue(self.domain_request._is_other_contacts_complete())
|
||||||
|
|
|
@ -9,6 +9,9 @@ from registrar.templatetags.custom_filters import (
|
||||||
find_index,
|
find_index,
|
||||||
slice_after,
|
slice_after,
|
||||||
contains_checkbox,
|
contains_checkbox,
|
||||||
|
is_domain_request_subpage,
|
||||||
|
is_domain_subpage,
|
||||||
|
is_portfolio_subpage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,3 +93,18 @@ class CustomFiltersTestCase(TestCase):
|
||||||
]
|
]
|
||||||
result = contains_checkbox(html_list)
|
result = contains_checkbox(html_list)
|
||||||
self.assertFalse(result) # Expecting False
|
self.assertFalse(result) # Expecting False
|
||||||
|
|
||||||
|
def test_is_domain_subpage(self):
|
||||||
|
"""Tests if the path is recognized as a domain subpage."""
|
||||||
|
self.assertTrue(is_domain_subpage("/domains/"))
|
||||||
|
self.assertFalse(is_domain_subpage("/"))
|
||||||
|
|
||||||
|
def test_is_domain_request_subpage(self):
|
||||||
|
"""Tests if the path is recognized as a domain request subpage."""
|
||||||
|
self.assertTrue(is_domain_request_subpage("/requests/"))
|
||||||
|
self.assertFalse(is_domain_request_subpage("/"))
|
||||||
|
|
||||||
|
def test_is_portfolio_subpage(self):
|
||||||
|
"""Tests if the path is recognized as a portfolio subpage."""
|
||||||
|
self.assertTrue(is_portfolio_subpage("/organization/"))
|
||||||
|
self.assertFalse(is_portfolio_subpage("/"))
|
||||||
|
|
|
@ -369,19 +369,12 @@ class HomeTests(TestWithUser):
|
||||||
last_name="Mars",
|
last_name="Mars",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach a user object to a contact (should not be deleted)
|
|
||||||
contact_user, _ = Contact.objects.get_or_create(
|
|
||||||
first_name="Hank",
|
|
||||||
last_name="McFakey",
|
|
||||||
)
|
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
domain_request = DomainRequest.objects.create(
|
domain_request = DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
requested_domain=site,
|
requested_domain=site,
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
senior_official=contact,
|
senior_official=contact,
|
||||||
submitter=contact_user,
|
|
||||||
)
|
)
|
||||||
domain_request.other_contacts.set([contact_2])
|
domain_request.other_contacts.set([contact_2])
|
||||||
|
|
||||||
|
@ -392,7 +385,6 @@ class HomeTests(TestWithUser):
|
||||||
requested_domain=site_2,
|
requested_domain=site_2,
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
senior_official=contact_2,
|
senior_official=contact_2,
|
||||||
submitter=contact_shared,
|
|
||||||
)
|
)
|
||||||
domain_request_2.other_contacts.set([contact_shared])
|
domain_request_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
|
@ -409,8 +401,6 @@ class HomeTests(TestWithUser):
|
||||||
# Check if the orphaned contacts were deleted
|
# Check if the orphaned contacts were deleted
|
||||||
orphan = Contact.objects.filter(id=contact.id)
|
orphan = Contact.objects.filter(id=contact.id)
|
||||||
self.assertFalse(orphan.exists())
|
self.assertFalse(orphan.exists())
|
||||||
orphan = Contact.objects.filter(id=contact_user.id)
|
|
||||||
self.assertFalse(orphan.exists())
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||||
|
@ -455,7 +445,6 @@ class HomeTests(TestWithUser):
|
||||||
requested_domain=site,
|
requested_domain=site,
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
senior_official=contact,
|
senior_official=contact,
|
||||||
submitter=contact_user,
|
|
||||||
)
|
)
|
||||||
domain_request.other_contacts.set([contact_2])
|
domain_request.other_contacts.set([contact_2])
|
||||||
|
|
||||||
|
@ -466,7 +455,6 @@ class HomeTests(TestWithUser):
|
||||||
requested_domain=site_2,
|
requested_domain=site_2,
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
senior_official=contact_2,
|
senior_official=contact_2,
|
||||||
submitter=contact_shared,
|
|
||||||
)
|
)
|
||||||
domain_request_2.other_contacts.set([contact_shared])
|
domain_request_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
|
@ -506,7 +494,13 @@ class HomeTests(TestWithUser):
|
||||||
phone = "8003111234"
|
phone = "8003111234"
|
||||||
status = User.RESTRICTED
|
status = User.RESTRICTED
|
||||||
restricted_user = get_user_model().objects.create(
|
restricted_user = get_user_model().objects.create(
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, status=status
|
username=username,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
email=email,
|
||||||
|
phone=phone,
|
||||||
|
status=status,
|
||||||
|
title="title",
|
||||||
)
|
)
|
||||||
self.client.force_login(restricted_user)
|
self.client.force_login(restricted_user)
|
||||||
response = self.client.get("/request/", follow=True)
|
response = self.client.get("/request/", follow=True)
|
||||||
|
@ -558,7 +552,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
return page.follow() if follow else page
|
return page.follow() if follow else page
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@override_flag("profile_feature", active=True)
|
|
||||||
def test_full_name_initial_value(self):
|
def test_full_name_initial_value(self):
|
||||||
"""Test that full_name initial value is empty when first_name or last_name is empty.
|
"""Test that full_name initial value is empty when first_name or last_name is empty.
|
||||||
This will later be displayed as "unknown" using javascript."""
|
This will later be displayed as "unknown" using javascript."""
|
||||||
|
@ -612,8 +605,8 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
incomplete_regular_user.delete()
|
incomplete_regular_user.delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_new_user_with_profile_feature_on(self):
|
def test_new_user(self):
|
||||||
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
|
"""Tests that a new user is redirected to the profile setup page"""
|
||||||
username_regular_incomplete = "test_regular_user_incomplete"
|
username_regular_incomplete = "test_regular_user_incomplete"
|
||||||
first_name_2 = "Incomplete"
|
first_name_2 = "Incomplete"
|
||||||
email_2 = "unicorn@igorville.com"
|
email_2 = "unicorn@igorville.com"
|
||||||
|
@ -626,39 +619,37 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.app.set_user(incomplete_regular_user.username)
|
self.app.set_user(incomplete_regular_user.username)
|
||||||
with override_flag("profile_feature", active=True):
|
# This will redirect the user to the setup page.
|
||||||
# This will redirect the user to the setup page.
|
# Follow implicity checks if our redirect is working.
|
||||||
# Follow implicity checks if our redirect is working.
|
finish_setup_page = self.app.get(reverse("home")).follow()
|
||||||
finish_setup_page = self.app.get(reverse("home")).follow()
|
self._set_session_cookie()
|
||||||
self._set_session_cookie()
|
# Assert that we're on the right page
|
||||||
|
self.assertContains(finish_setup_page, "Finish setting up your profile")
|
||||||
|
|
||||||
# Assert that we're on the right page
|
finish_setup_page = self._submit_form_webtest(finish_setup_page.form)
|
||||||
self.assertContains(finish_setup_page, "Finish setting up your profile")
|
|
||||||
|
|
||||||
finish_setup_page = self._submit_form_webtest(finish_setup_page.form)
|
self.assertEqual(finish_setup_page.status_code, 200)
|
||||||
|
|
||||||
self.assertEqual(finish_setup_page.status_code, 200)
|
# We're missing a phone number, so the page should tell us that
|
||||||
|
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||||
|
|
||||||
# We're missing a phone number, so the page should tell us that
|
# Check for the name of the save button
|
||||||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||||
|
|
||||||
# Check for the name of the save button
|
# Add a phone number
|
||||||
self.assertContains(finish_setup_page, "user_setup_save_button")
|
finish_setup_form = finish_setup_page.form
|
||||||
|
finish_setup_form["phone"] = "(201) 555-0123"
|
||||||
|
finish_setup_form["title"] = "CEO"
|
||||||
|
finish_setup_form["last_name"] = "example"
|
||||||
|
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
||||||
|
|
||||||
# Add a phone number
|
self.assertEqual(save_page.status_code, 200)
|
||||||
finish_setup_form = finish_setup_page.form
|
self.assertContains(save_page, "Your profile has been updated.")
|
||||||
finish_setup_form["phone"] = "(201) 555-0123"
|
|
||||||
finish_setup_form["title"] = "CEO"
|
|
||||||
finish_setup_form["last_name"] = "example"
|
|
||||||
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
|
||||||
|
|
||||||
self.assertEqual(save_page.status_code, 200)
|
# Try to navigate back to the home page.
|
||||||
self.assertContains(save_page, "Your profile has been updated.")
|
# This is the same as clicking the back button.
|
||||||
|
completed_setup_page = self.app.get(reverse("home"))
|
||||||
# Try to navigate back to the home page.
|
self.assertContains(completed_setup_page, "Manage your domain")
|
||||||
# This is the same as clicking the back button.
|
|
||||||
completed_setup_page = self.app.get(reverse("home"))
|
|
||||||
self.assertContains(completed_setup_page, "Manage your domain")
|
|
||||||
incomplete_regular_user.delete()
|
incomplete_regular_user.delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -675,46 +666,45 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
verification_type=User.VerificationTypeChoices.REGULAR,
|
verification_type=User.VerificationTypeChoices.REGULAR,
|
||||||
)
|
)
|
||||||
self.app.set_user(incomplete_regular_user.username)
|
self.app.set_user(incomplete_regular_user.username)
|
||||||
with override_flag("profile_feature", active=True):
|
# This will redirect the user to the setup page.
|
||||||
# This will redirect the user to the setup page.
|
# Follow implicity checks if our redirect is working.
|
||||||
# Follow implicity checks if our redirect is working.
|
finish_setup_page = self.app.get(reverse("home")).follow()
|
||||||
finish_setup_page = self.app.get(reverse("home")).follow()
|
self._set_session_cookie()
|
||||||
self._set_session_cookie()
|
|
||||||
|
|
||||||
# Assert that we're on the right page
|
# Assert that we're on the right page
|
||||||
self.assertContains(finish_setup_page, "Finish setting up your profile")
|
self.assertContains(finish_setup_page, "Finish setting up your profile")
|
||||||
|
|
||||||
finish_setup_page = self._submit_form_webtest(finish_setup_page.form)
|
finish_setup_page = self._submit_form_webtest(finish_setup_page.form)
|
||||||
|
|
||||||
self.assertEqual(finish_setup_page.status_code, 200)
|
self.assertEqual(finish_setup_page.status_code, 200)
|
||||||
|
|
||||||
# We're missing a phone number, so the page should tell us that
|
# We're missing a phone number, so the page should tell us that
|
||||||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||||
|
|
||||||
# Check for the name of the save button
|
# Check for the name of the save button
|
||||||
self.assertContains(finish_setup_page, "user_setup_save_button")
|
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||||
|
|
||||||
# Add a phone number
|
# Add a phone number
|
||||||
finish_setup_form = finish_setup_page.form
|
finish_setup_form = finish_setup_page.form
|
||||||
finish_setup_form["first_name"] = "test"
|
finish_setup_form["first_name"] = "test"
|
||||||
finish_setup_form["last_name"] = "test2"
|
finish_setup_form["last_name"] = "test2"
|
||||||
finish_setup_form["phone"] = "(201) 555-0123"
|
finish_setup_form["phone"] = "(201) 555-0123"
|
||||||
finish_setup_form["title"] = "CEO"
|
finish_setup_form["title"] = "CEO"
|
||||||
finish_setup_form["last_name"] = "example"
|
finish_setup_form["last_name"] = "example"
|
||||||
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
||||||
|
|
||||||
self.assertEqual(save_page.status_code, 200)
|
self.assertEqual(save_page.status_code, 200)
|
||||||
self.assertContains(save_page, "Your profile has been updated.")
|
self.assertContains(save_page, "Your profile has been updated.")
|
||||||
|
|
||||||
# Try to navigate back to the home page.
|
# Try to navigate back to the home page.
|
||||||
# This is the same as clicking the back button.
|
# This is the same as clicking the back button.
|
||||||
completed_setup_page = self.app.get(reverse("home"))
|
completed_setup_page = self.app.get(reverse("home"))
|
||||||
self.assertContains(completed_setup_page, "Manage your domain")
|
self.assertContains(completed_setup_page, "Manage your domain")
|
||||||
incomplete_regular_user.delete()
|
incomplete_regular_user.delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
|
def test_new_user_goes_to_domain_request(self):
|
||||||
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
|
"""Tests that a new user is redirected to the domain request page"""
|
||||||
username_regular_incomplete = "test_regular_user_incomplete"
|
username_regular_incomplete = "test_regular_user_incomplete"
|
||||||
first_name_2 = "Incomplete"
|
first_name_2 = "Incomplete"
|
||||||
email_2 = "unicorn@igorville.com"
|
email_2 = "unicorn@igorville.com"
|
||||||
|
@ -726,7 +716,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
verification_type=User.VerificationTypeChoices.REGULAR,
|
verification_type=User.VerificationTypeChoices.REGULAR,
|
||||||
)
|
)
|
||||||
self.app.set_user(incomplete_regular_user.username)
|
self.app.set_user(incomplete_regular_user.username)
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("", active=True):
|
||||||
# This will redirect the user to the setup page
|
# This will redirect the user to the setup page
|
||||||
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
|
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
|
||||||
self._set_session_cookie()
|
self._set_session_cookie()
|
||||||
|
@ -770,25 +760,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
self.assertContains(completed_setup_page, "You’re about to start your .gov domain request")
|
self.assertContains(completed_setup_page, "You’re about to start your .gov domain request")
|
||||||
incomplete_regular_user.delete()
|
incomplete_regular_user.delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_new_user_with_profile_feature_off(self):
|
|
||||||
"""Tests that a new user is not redirected to the profile setup page when profile_feature is off"""
|
|
||||||
with override_flag("profile_feature", active=False):
|
|
||||||
response = self.client.get("/")
|
|
||||||
self.assertNotContains(response, "Finish setting up your profile")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_new_user_goes_to_domain_request_with_profile_feature_off(self):
|
|
||||||
"""Tests that a new user is redirected to the domain request page
|
|
||||||
when profile_feature is off but not the setup page"""
|
|
||||||
with override_flag("profile_feature", active=False):
|
|
||||||
response = self.client.get("/request/")
|
|
||||||
|
|
||||||
self.assertNotContains(response, "Finish setting up your profile")
|
|
||||||
self.assertNotContains(response, "What contact information should we use to reach you?")
|
|
||||||
|
|
||||||
self.assertContains(response, "You’re about to start your .gov domain request")
|
|
||||||
|
|
||||||
|
|
||||||
class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||||
"""A series of tests that target the user profile page intercept for incomplete IAL1 user profiles."""
|
"""A series of tests that target the user profile page intercept for incomplete IAL1 user profiles."""
|
||||||
|
@ -828,8 +799,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||||
return page.follow() if follow else page
|
return page.follow() if follow else page
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_new_user_with_profile_feature_on(self):
|
def test_new_user(self):
|
||||||
"""Tests that a new user is redirected to the profile setup page when profile_feature is on,
|
"""Tests that a new user is redirected to the profile setup page,
|
||||||
and testing that the confirmation modal is present"""
|
and testing that the confirmation modal is present"""
|
||||||
username_other_incomplete = "test_other_user_incomplete"
|
username_other_incomplete = "test_other_user_incomplete"
|
||||||
first_name_2 = "Incomplete"
|
first_name_2 = "Incomplete"
|
||||||
|
@ -843,66 +814,63 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||||
verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
|
verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
|
||||||
)
|
)
|
||||||
self.app.set_user(incomplete_other_user.username)
|
self.app.set_user(incomplete_other_user.username)
|
||||||
with override_flag("profile_feature", active=True):
|
# This will redirect the user to the user profile page.
|
||||||
# This will redirect the user to the user profile page.
|
# Follow implicity checks if our redirect is working.
|
||||||
# Follow implicity checks if our redirect is working.
|
user_profile_page = self.app.get(reverse("home")).follow()
|
||||||
user_profile_page = self.app.get(reverse("home")).follow()
|
self._set_session_cookie()
|
||||||
self._set_session_cookie()
|
|
||||||
|
|
||||||
# Assert that we're on the right page by testing for the modal
|
# Assert that we're on the right page by testing for the modal
|
||||||
self.assertContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
self.assertContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
||||||
|
|
||||||
user_profile_page = self._submit_form_webtest(user_profile_page.form)
|
user_profile_page = self._submit_form_webtest(user_profile_page.form)
|
||||||
|
|
||||||
self.assertEqual(user_profile_page.status_code, 200)
|
self.assertEqual(user_profile_page.status_code, 200)
|
||||||
|
|
||||||
# Assert that modal does not appear on subsequent submits
|
# Assert that modal does not appear on subsequent submits
|
||||||
self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
||||||
# Assert that unique error message appears by testing the message in a specific div
|
# Assert that unique error message appears by testing the message in a specific div
|
||||||
html_content = user_profile_page.content.decode("utf-8")
|
html_content = user_profile_page.content.decode("utf-8")
|
||||||
# Normalize spaces and line breaks in the HTML content
|
# Normalize spaces and line breaks in the HTML content
|
||||||
normalized_html_content = " ".join(html_content.split())
|
normalized_html_content = " ".join(html_content.split())
|
||||||
# Expected string without extra spaces and line breaks
|
# Expected string without extra spaces and line breaks
|
||||||
expected_string = "Before you can manage your domain, we need you to add contact information."
|
expected_string = "Before you can manage your domain, we need you to add contact information."
|
||||||
# Check for the presence of the <div> element with the specific text
|
# Check for the presence of the <div> element with the specific text
|
||||||
self.assertIn(f'<div class="usa-alert__body"> {expected_string} </div>', normalized_html_content)
|
self.assertIn(f'<div class="usa-alert__body"> {expected_string} </div>', normalized_html_content)
|
||||||
|
|
||||||
# We're missing a phone number, so the page should tell us that
|
# We're missing a phone number, so the page should tell us that
|
||||||
self.assertContains(user_profile_page, "Enter your phone number.")
|
self.assertContains(user_profile_page, "Enter your phone number.")
|
||||||
|
|
||||||
# We need to assert that links to manage your domain are not present (in both body and footer)
|
# We need to assert that links to manage your domain are not present (in both body and footer)
|
||||||
self.assertNotContains(user_profile_page, "Manage your domains")
|
self.assertNotContains(user_profile_page, "Manage your domains")
|
||||||
# Assert the tooltip on the logo, indicating that the logo is not clickable
|
# Assert the tooltip on the logo, indicating that the logo is not clickable
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
user_profile_page, 'title="Before you can manage your domains, we need you to add contact information."'
|
user_profile_page, 'title="Before you can manage your domains, we need you to add contact information."'
|
||||||
)
|
)
|
||||||
# Assert that modal does not appear on subsequent submits
|
# Assert that modal does not appear on subsequent submits
|
||||||
self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
||||||
|
|
||||||
# Add a phone number
|
# Add a phone number
|
||||||
finish_setup_form = user_profile_page.form
|
finish_setup_form = user_profile_page.form
|
||||||
finish_setup_form["phone"] = "(201) 555-0123"
|
finish_setup_form["phone"] = "(201) 555-0123"
|
||||||
finish_setup_form["title"] = "CEO"
|
finish_setup_form["title"] = "CEO"
|
||||||
finish_setup_form["last_name"] = "example"
|
finish_setup_form["last_name"] = "example"
|
||||||
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
||||||
|
|
||||||
self.assertEqual(save_page.status_code, 200)
|
self.assertEqual(save_page.status_code, 200)
|
||||||
self.assertContains(save_page, "Your profile has been updated.")
|
self.assertContains(save_page, "Your profile has been updated.")
|
||||||
|
|
||||||
# We need to assert that logo is not clickable and links to manage your domain are not present
|
# We need to assert that logo is not clickable and links to manage your domain are not present
|
||||||
# NOTE: "anage" is not a typo. It is to accomodate the fact that the "m" is uppercase in one
|
# NOTE: "anage" is not a typo. It is to accomodate the fact that the "m" is uppercase in one
|
||||||
# instance and lowercase in the other.
|
# instance and lowercase in the other.
|
||||||
self.assertContains(save_page, "anage your domains", count=2)
|
self.assertContains(save_page, "anage your domains", count=2)
|
||||||
self.assertNotContains(
|
self.assertNotContains(save_page, "Before you can manage your domains, we need you to add contact information")
|
||||||
save_page, "Before you can manage your domains, we need you to add contact information"
|
# Assert that modal does not appear on subsequent submits
|
||||||
)
|
self.assertNotContains(save_page, "domain registrants must maintain accurate contact information")
|
||||||
# Assert that modal does not appear on subsequent submits
|
|
||||||
self.assertNotContains(save_page, "domain registrants must maintain accurate contact information")
|
|
||||||
|
|
||||||
# Try to navigate back to the home page.
|
# Try to navigate back to the home page.
|
||||||
# This is the same as clicking the back button.
|
# This is the same as clicking the back button.
|
||||||
completed_setup_page = self.app.get(reverse("home"))
|
completed_setup_page = self.app.get(reverse("home"))
|
||||||
self.assertContains(completed_setup_page, "Manage your domain")
|
self.assertContains(completed_setup_page, "Manage your domain")
|
||||||
|
|
||||||
|
|
||||||
class UserProfileTests(TestWithUser, WebTest):
|
class UserProfileTests(TestWithUser, WebTest):
|
||||||
|
@ -927,106 +895,59 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def error_500_main_nav_with_profile_feature_turned_on(self):
|
def error_500_main_nav(self):
|
||||||
"""test that Your profile is in main nav of 500 error page when profile_feature is on.
|
"""test that Your profile is in main nav of 500 error page.
|
||||||
|
|
||||||
Our treatment of 401 and 403 error page handling with that waffle feature is similar, so we
|
Our treatment of 401 and 403 error page handling with that waffle feature is similar, so we
|
||||||
assume that the same test results hold true for 401 and 403."""
|
assume that the same test results hold true for 401 and 403."""
|
||||||
with override_flag("profile_feature", active=True):
|
with self.assertRaises(Exception):
|
||||||
with self.assertRaises(Exception):
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
response = self.client.get(reverse("home"), follow=True)
|
self.assertEqual(response.status_code, 500)
|
||||||
self.assertEqual(response.status_code, 500)
|
self.assertContains(response, "Your profile")
|
||||||
self.assertContains(response, "Your profile")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def error_500_main_nav_with_profile_feature_turned_off(self):
|
def test_home_page_main_nav(self):
|
||||||
"""test that Your profile is not in main nav of 500 error page when profile_feature is off.
|
"""test that Your profile is in main nav of home page"""
|
||||||
|
response = self.client.get("/", follow=True)
|
||||||
Our treatment of 401 and 403 error page handling with that waffle feature is similar, so we
|
|
||||||
assume that the same test results hold true for 401 and 403."""
|
|
||||||
with override_flag("profile_feature", active=False):
|
|
||||||
with self.assertRaises(Exception):
|
|
||||||
response = self.client.get(reverse("home"), follow=True)
|
|
||||||
self.assertEqual(response.status_code, 500)
|
|
||||||
self.assertNotContains(response, "Your profile")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_home_page_main_nav_with_profile_feature_on(self):
|
|
||||||
"""test that Your profile is in main nav of home page when profile_feature is on"""
|
|
||||||
with override_flag("profile_feature", active=True):
|
|
||||||
response = self.client.get("/", follow=True)
|
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_home_page_main_nav_with_profile_feature_off(self):
|
def test_new_request_main_nav(self):
|
||||||
"""test that Your profile is not in main nav of home page when profile_feature is off"""
|
"""test that Your profile is in main nav of new request"""
|
||||||
with override_flag("profile_feature", active=False):
|
response = self.client.get("/request/", follow=True)
|
||||||
response = self.client.get("/", follow=True)
|
|
||||||
self.assertNotContains(response, "Your profile")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_new_request_main_nav_with_profile_feature_on(self):
|
|
||||||
"""test that Your profile is in main nav of new request when profile_feature is on"""
|
|
||||||
with override_flag("profile_feature", active=True):
|
|
||||||
response = self.client.get("/request/", follow=True)
|
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_new_request_main_nav_with_profile_feature_off(self):
|
def test_user_profile_main_nav(self):
|
||||||
"""test that Your profile is not in main nav of new request when profile_feature is off"""
|
"""test that Your profile is in main nav of user profile"""
|
||||||
with override_flag("profile_feature", active=False):
|
response = self.client.get("/user-profile", follow=True)
|
||||||
response = self.client.get("/request/", follow=True)
|
|
||||||
self.assertNotContains(response, "Your profile")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_user_profile_main_nav_with_profile_feature_on(self):
|
|
||||||
"""test that Your profile is in main nav of user profile when profile_feature is on"""
|
|
||||||
with override_flag("profile_feature", active=True):
|
|
||||||
response = self.client.get("/user-profile", follow=True)
|
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_user_profile_returns_404_when_feature_off(self):
|
|
||||||
"""test that Your profile returns 404 when profile_feature is off"""
|
|
||||||
with override_flag("profile_feature", active=False):
|
|
||||||
response = self.client.get("/user-profile", follow=True)
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_user_profile_back_button_when_coming_from_domain_request(self):
|
def test_user_profile_back_button_when_coming_from_domain_request(self):
|
||||||
"""tests user profile when profile_feature is on,
|
"""tests user profile,
|
||||||
and when they are redirected from the domain request page"""
|
and when they are redirected from the domain request page"""
|
||||||
with override_flag("profile_feature", active=True):
|
response = self.client.get("/user-profile?redirect=domain-request:")
|
||||||
response = self.client.get("/user-profile?redirect=domain-request:")
|
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
self.assertContains(response, "Go back to your domain request")
|
self.assertContains(response, "Go back to your domain request")
|
||||||
self.assertNotContains(response, "Back to manage your domains")
|
self.assertNotContains(response, "Back to manage your domains")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_detail_profile_feature_on(self):
|
def test_domain_detail_contains_your_profile(self):
|
||||||
"""test that domain detail view when profile_feature is on"""
|
"""Tests that the domain detail view contains 'your profile' rather than 'your contact information'"""
|
||||||
with override_flag("profile_feature", active=True):
|
response = self.client.get(reverse("domain", args=[self.domain.pk]))
|
||||||
response = self.client.get(reverse("domain", args=[self.domain.pk]))
|
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
self.assertNotContains(response, "Your contact information")
|
self.assertNotContains(response, "Your contact information")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_your_contact_information_when_profile_feature_off(self):
|
def test_domain_your_contact_information(self):
|
||||||
"""test that Your contact information is accessible when profile_feature is off"""
|
"""test that your contact information is not accessible"""
|
||||||
with override_flag("profile_feature", active=False):
|
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
||||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
|
||||||
self.assertContains(response, "Your contact information")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_domain_your_contact_information_when_profile_feature_on(self):
|
|
||||||
"""test that Your contact information is not accessible when profile feature is on"""
|
|
||||||
with override_flag("profile_feature", active=True):
|
|
||||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_request_when_profile_feature_on(self):
|
def test_profile_request_page(self):
|
||||||
"""test that Your profile is in request page when profile feature is on"""
|
"""test that your profile is in request"""
|
||||||
|
|
||||||
contact_user, _ = Contact.objects.get_or_create(
|
contact_user, _ = Contact.objects.get_or_create(
|
||||||
first_name="Hank",
|
first_name="Hank",
|
||||||
|
@ -1038,55 +959,28 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
requested_domain=site,
|
requested_domain=site,
|
||||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
senior_official=contact_user,
|
senior_official=contact_user,
|
||||||
submitter=contact_user,
|
|
||||||
)
|
)
|
||||||
with override_flag("profile_feature", active=True):
|
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
|
||||||
self.assertContains(response, "Your profile")
|
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
|
||||||
self.assertContains(response, "Your profile")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||||
def test_request_when_profile_feature_off(self):
|
self.assertContains(response, "Your profile")
|
||||||
"""test that Your profile is not in request page when profile feature is off"""
|
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
||||||
|
self.assertContains(response, "Your profile")
|
||||||
contact_user, _ = Contact.objects.get_or_create(
|
|
||||||
first_name="Hank",
|
|
||||||
last_name="McFakerson",
|
|
||||||
)
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
domain_request = DomainRequest.objects.create(
|
|
||||||
creator=self.user,
|
|
||||||
requested_domain=site,
|
|
||||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
|
||||||
senior_official=contact_user,
|
|
||||||
submitter=contact_user,
|
|
||||||
)
|
|
||||||
with override_flag("profile_feature", active=False):
|
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
|
||||||
self.assertNotContains(response, "Your profile")
|
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
|
||||||
self.assertNotContains(response, "Your profile")
|
|
||||||
# cleanup
|
|
||||||
domain_request.delete()
|
|
||||||
site.delete()
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_user_profile_form_submission(self):
|
def test_user_profile_form_submission(self):
|
||||||
"""test user profile form submission"""
|
"""test user profile form submission"""
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
with override_flag("profile_feature", active=True):
|
profile_page = self.app.get(reverse("user-profile"))
|
||||||
profile_page = self.app.get(reverse("user-profile"))
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
profile_form = profile_page.form
|
||||||
profile_form = profile_page.form
|
profile_form["title"] = "sample title"
|
||||||
profile_form["title"] = "sample title"
|
profile_form["phone"] = "(201) 555-1212"
|
||||||
profile_form["phone"] = "(201) 555-1212"
|
profile_page = profile_form.submit()
|
||||||
profile_page = profile_form.submit()
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
profile_page = profile_page.follow()
|
||||||
profile_page = profile_page.follow()
|
self.assertEqual(profile_page.status_code, 200)
|
||||||
self.assertEqual(profile_page.status_code, 200)
|
self.assertContains(profile_page, "Your profile has been updated")
|
||||||
self.assertContains(profile_page, "Your profile has been updated")
|
|
||||||
|
|
||||||
|
|
||||||
class PortfoliosTests(TestWithUser, WebTest):
|
class PortfoliosTests(TestWithUser, WebTest):
|
||||||
|
|
|
@ -163,7 +163,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
||||||
"domain-dns-nameservers",
|
"domain-dns-nameservers",
|
||||||
"domain-org-name-address",
|
"domain-org-name-address",
|
||||||
"domain-senior-official",
|
"domain-senior-official",
|
||||||
"domain-your-contact-information",
|
|
||||||
"domain-security-email",
|
"domain-security-email",
|
||||||
]:
|
]:
|
||||||
with self.subTest(view_name=view_name):
|
with self.subTest(view_name=view_name):
|
||||||
|
@ -183,7 +182,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
||||||
"domain-dns-nameservers",
|
"domain-dns-nameservers",
|
||||||
"domain-org-name-address",
|
"domain-org-name-address",
|
||||||
"domain-senior-official",
|
"domain-senior-official",
|
||||||
"domain-your-contact-information",
|
|
||||||
"domain-security-email",
|
"domain-security-email",
|
||||||
]:
|
]:
|
||||||
with self.subTest(view_name=view_name):
|
with self.subTest(view_name=view_name):
|
||||||
|
@ -204,7 +202,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
||||||
"domain-dns-dnssec-dsdata",
|
"domain-dns-dnssec-dsdata",
|
||||||
"domain-org-name-address",
|
"domain-org-name-address",
|
||||||
"domain-senior-official",
|
"domain-senior-official",
|
||||||
"domain-your-contact-information",
|
|
||||||
"domain-security-email",
|
"domain-security-email",
|
||||||
]:
|
]:
|
||||||
for domain in [
|
for domain in [
|
||||||
|
@ -728,7 +725,7 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
|
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
|
||||||
|
|
||||||
other_user = User()
|
other_user = create_user()
|
||||||
other_user.save()
|
other_user.save()
|
||||||
self.client.force_login(other_user)
|
self.client.force_login(other_user)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
|
@ -742,6 +739,12 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
def test_domain_invitation_flow(self):
|
def test_domain_invitation_flow(self):
|
||||||
"""Send an invitation to a new user, log in and load the dashboard."""
|
"""Send an invitation to a new user, log in and load the dashboard."""
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
username = "mayor"
|
||||||
|
first_name = "First"
|
||||||
|
last_name = "Last"
|
||||||
|
title = "title"
|
||||||
|
phone = "8080102431"
|
||||||
|
title = "title"
|
||||||
User.objects.filter(email=email_address).delete()
|
User.objects.filter(email=email_address).delete()
|
||||||
|
|
||||||
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||||
|
@ -757,7 +760,9 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
add_page.form.submit()
|
add_page.form.submit()
|
||||||
|
|
||||||
# user was invited, create them
|
# user was invited, create them
|
||||||
new_user = User.objects.create(username=email_address, email=email_address)
|
new_user = User.objects.create(
|
||||||
|
username=username, email=email_address, first_name=first_name, last_name=last_name, title=title, phone=phone
|
||||||
|
)
|
||||||
# log them in to `self.app`
|
# log them in to `self.app`
|
||||||
self.app.set_user(new_user.username)
|
self.app.set_user(new_user.username)
|
||||||
# and manually call the on each login callback
|
# and manually call the on each login callback
|
||||||
|
@ -1303,7 +1308,9 @@ class TestDomainOrganization(TestDomainOverview):
|
||||||
"""Can load domain's org name and mailing address page."""
|
"""Can load domain's org name and mailing address page."""
|
||||||
page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||||
# once on the sidebar, once in the page title, once as H1
|
# once on the sidebar, once in the page title, once as H1
|
||||||
self.assertContains(page, "Organization name and mailing address", count=4)
|
self.assertContains(page, "/org-name-address")
|
||||||
|
self.assertContains(page, "Organization name and mailing address")
|
||||||
|
self.assertContains(page, "Organization</h1>")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_org_name_address_content(self):
|
def test_domain_org_name_address_content(self):
|
||||||
|
@ -1612,7 +1619,7 @@ class TestDomainSuborganization(TestDomainOverview):
|
||||||
|
|
||||||
# Test for the title change
|
# Test for the title change
|
||||||
self.assertContains(page, "Suborganization")
|
self.assertContains(page, "Suborganization")
|
||||||
self.assertNotContains(page, "Organization name")
|
self.assertNotContains(page, "Organization")
|
||||||
|
|
||||||
# Test for the good value
|
# Test for the good value
|
||||||
self.assertContains(page, "Ice Cream")
|
self.assertContains(page, "Ice Cream")
|
||||||
|
@ -1626,22 +1633,6 @@ class TestDomainSuborganization(TestDomainOverview):
|
||||||
portfolio.delete()
|
portfolio.delete()
|
||||||
|
|
||||||
|
|
||||||
class TestDomainContactInformation(TestDomainOverview):
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_domain_your_contact_information(self):
|
|
||||||
"""Can load domain's your contact information page."""
|
|
||||||
page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
|
||||||
self.assertContains(page, "Your contact information")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_domain_your_contact_information_content(self):
|
|
||||||
"""Logged-in user's contact information appears on the page."""
|
|
||||||
self.user.first_name = "Testy"
|
|
||||||
self.user.save()
|
|
||||||
page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
|
||||||
self.assertContains(page, "Testy")
|
|
||||||
|
|
||||||
|
|
||||||
class TestDomainSecurityEmail(TestDomainOverview):
|
class TestDomainSecurityEmail(TestDomainOverview):
|
||||||
def test_domain_security_email_existing_security_contact(self):
|
def test_domain_security_email_existing_security_contact(self):
|
||||||
"""Can load domain's security email page."""
|
"""Can load domain's security email page."""
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
from registrar.models import UserDomainRole, Domain, DomainInformation, Portfolio
|
from registrar.models import UserDomainRole, Domain, DomainInformation, Portfolio
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.utils.dateparse import parse_date
|
from django.utils.dateparse import parse_date
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
from waffle.testutils import override_flag
|
||||||
|
|
||||||
|
|
||||||
class GetDomainsJsonTest(TestWithUser, WebTest):
|
class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
|
@ -31,6 +35,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
UserPortfolioPermission.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
@ -115,8 +120,104 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertEqual(svg_icon_expected, svg_icons[i])
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_get_domains_json_with_portfolio(self):
|
@override_flag("organization_feature", active=True)
|
||||||
"""Test that an authenticated user gets the list of 2 domains for portfolio."""
|
def test_get_domains_json_with_portfolio_view_managed_domains(self):
|
||||||
|
"""Test that an authenticated user gets the list of 1 domain for portfolio. The 1 domain
|
||||||
|
is the domain that they manage within the portfolio."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
|
# Check the number of domains
|
||||||
|
self.assertEqual(len(data["domains"]), 1)
|
||||||
|
|
||||||
|
# Expected domains
|
||||||
|
expected_domains = [self.domain3]
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
domain_ids = [domain["id"] for domain in data["domains"]]
|
||||||
|
names = [domain["name"] for domain in data["domains"]]
|
||||||
|
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
|
||||||
|
states = [domain["state"] for domain in data["domains"]]
|
||||||
|
state_displays = [domain["state_display"] for domain in data["domains"]]
|
||||||
|
get_state_help_texts = [domain["get_state_help_text"] for domain in data["domains"]]
|
||||||
|
action_urls = [domain["action_url"] for domain in data["domains"]]
|
||||||
|
action_labels = [domain["action_label"] for domain in data["domains"]]
|
||||||
|
svg_icons = [domain["svg_icon"] for domain in data["domains"]]
|
||||||
|
|
||||||
|
# Check fields for each domain
|
||||||
|
for i, expected_domain in enumerate(expected_domains):
|
||||||
|
self.assertEqual(expected_domain.id, domain_ids[i])
|
||||||
|
self.assertEqual(expected_domain.name, names[i])
|
||||||
|
self.assertEqual(expected_domain.expiration_date, expiration_dates[i])
|
||||||
|
self.assertEqual(expected_domain.state, states[i])
|
||||||
|
|
||||||
|
# Parsing the expiration date from string to date
|
||||||
|
parsed_expiration_date = parse_date(expiration_dates[i])
|
||||||
|
expected_domain.expiration_date = parsed_expiration_date
|
||||||
|
|
||||||
|
# Check state_display and get_state_help_text
|
||||||
|
self.assertEqual(expected_domain.state_display(), state_displays[i])
|
||||||
|
self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i])
|
||||||
|
|
||||||
|
self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i])
|
||||||
|
|
||||||
|
# Check action_label
|
||||||
|
user_domain_role_exists = UserDomainRole.objects.filter(
|
||||||
|
domain_id=expected_domains[i].id, user=self.user
|
||||||
|
).exists()
|
||||||
|
action_label_expected = (
|
||||||
|
"View"
|
||||||
|
if not user_domain_role_exists
|
||||||
|
or expected_domains[i].state
|
||||||
|
in [
|
||||||
|
Domain.State.DELETED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
]
|
||||||
|
else "Manage"
|
||||||
|
)
|
||||||
|
self.assertEqual(action_label_expected, action_labels[i])
|
||||||
|
|
||||||
|
# Check svg_icon
|
||||||
|
svg_icon_expected = (
|
||||||
|
"visibility"
|
||||||
|
if not user_domain_role_exists
|
||||||
|
or expected_domains[i].state
|
||||||
|
in [
|
||||||
|
Domain.State.DELETED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
]
|
||||||
|
else "settings"
|
||||||
|
)
|
||||||
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_get_domains_json_with_portfolio_view_all_domains(self):
|
||||||
|
"""Test that an authenticated user gets the list of 2 domains for portfolio. One is a domain which
|
||||||
|
they manage within the portfolio. The other is a domain which they don't manage within the
|
||||||
|
portfolio."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
|
||||||
|
)
|
||||||
|
|
||||||
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
|
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -12,10 +12,11 @@ from registrar.models import (
|
||||||
)
|
)
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from .common import create_test_user
|
from .common import MockSESClient, completed_domain_request, create_test_user
|
||||||
from waffle.testutils import override_flag
|
from waffle.testutils import override_flag
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
|
import boto3_mocking # type: ignore
|
||||||
|
from django.test import Client
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -24,6 +25,7 @@ logger = logging.getLogger(__name__)
|
||||||
class TestPortfolio(WebTest):
|
class TestPortfolio(WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.client = Client()
|
||||||
self.user = create_test_user()
|
self.user = create_test_user()
|
||||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||||
|
@ -76,7 +78,7 @@ class TestPortfolio(WebTest):
|
||||||
def test_middleware_does_not_redirect_if_no_permission(self):
|
def test_middleware_does_not_redirect_if_no_permission(self):
|
||||||
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
|
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
user=self.user, portfolio=self.portfolio, additional_permissions=[]
|
user=self.user, portfolio=self.portfolio, additional_permissions=[]
|
||||||
)
|
)
|
||||||
self.user.portfolio = self.portfolio
|
self.user.portfolio = self.portfolio
|
||||||
|
@ -198,7 +200,7 @@ class TestPortfolio(WebTest):
|
||||||
# Assert the response is a 200
|
# Assert the response is a 200
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# The label for Federal agency will always be a h4
|
# The label for Federal agency will always be a h4
|
||||||
self.assertContains(response, '<h4 class="read-only-label">Federal agency</h4>')
|
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
|
||||||
# The read only label for city will be a h4
|
# The read only label for city will be a h4
|
||||||
self.assertContains(response, '<h4 class="read-only-label">City</h4>')
|
self.assertContains(response, '<h4 class="read-only-label">City</h4>')
|
||||||
self.assertNotContains(response, 'for="id_city"')
|
self.assertNotContains(response, 'for="id_city"')
|
||||||
|
@ -223,10 +225,10 @@ class TestPortfolio(WebTest):
|
||||||
# Assert the response is a 200
|
# Assert the response is a 200
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# The label for Federal agency will always be a h4
|
# The label for Federal agency will always be a h4
|
||||||
self.assertContains(response, '<h4 class="read-only-label">Federal agency</h4>')
|
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
|
||||||
# The read only label for city will be a h4
|
# The read only label for city will be a h4
|
||||||
self.assertNotContains(response, '<h4 class="read-only-label">City</h4>')
|
self.assertNotContains(response, '<h4 class="read-only-label">City</h4>')
|
||||||
self.assertNotContains(response, '<p class="read-only-value">Los Angeles</p>>')
|
self.assertNotContains(response, '<p class="read-only-value">Los Angeles</p>')
|
||||||
self.assertContains(response, 'for="id_city"')
|
self.assertContains(response, 'for="id_city"')
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -340,9 +342,7 @@ class TestPortfolio(WebTest):
|
||||||
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
|
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
|
||||||
)
|
)
|
||||||
page = self.app.get(reverse("organization"))
|
page = self.app.get(reverse("organization"))
|
||||||
self.assertContains(
|
self.assertContains(page, "The name of your organization will be publicly listed as the domain registrant.")
|
||||||
page, "The name of your federal agency will be publicly listed as the domain registrant."
|
|
||||||
)
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_org_name_address_content(self):
|
def test_domain_org_name_address_content(self):
|
||||||
|
@ -504,7 +504,7 @@ class TestPortfolio(WebTest):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(reverse("home"), follow=True)
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
self.assertFalse(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
self.assertFalse(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "You aren")
|
self.assertContains(response, "You aren")
|
||||||
|
|
||||||
|
@ -519,7 +519,7 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
# Test the domains page - this user should have access
|
# Test the domains page - this user should have access
|
||||||
response = self.client.get(reverse("domains"))
|
response = self.client.get(reverse("domains"))
|
||||||
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Domain name")
|
self.assertContains(response, "Domain name")
|
||||||
|
|
||||||
|
@ -530,7 +530,7 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
# Test the domains page - this user should have access
|
# Test the domains page - this user should have access
|
||||||
response = self.client.get(reverse("domains"))
|
response = self.client.get(reverse("domains"))
|
||||||
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Domain name")
|
self.assertContains(response, "Domain name")
|
||||||
permission.delete()
|
permission.delete()
|
||||||
|
@ -547,7 +547,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
],
|
],
|
||||||
|
@ -573,7 +573,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
],
|
],
|
||||||
|
@ -599,7 +599,7 @@ class TestPortfolio(WebTest):
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
additional_permissions=[
|
additional_permissions=[
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
],
|
],
|
||||||
|
@ -630,3 +630,311 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
self.assertContains(home, "Hotel California")
|
self.assertContains(home, "Hotel California")
|
||||||
self.assertContains(home, "Members")
|
self.assertContains(home, "Members")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_portfolio_domain_requests_page_when_user_has_no_permissions(self):
|
||||||
|
"""Test the no requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
requests_page = self.client.get(reverse("no-portfolio-requests"), follow=True)
|
||||||
|
|
||||||
|
self.assertContains(requests_page, "You don’t have access to domain requests.")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_main_nav_when_user_has_no_permissions(self):
|
||||||
|
"""Test the nav contains a link to the no requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# link to no requests
|
||||||
|
self.assertContains(portfolio_landing_page, "no-organization-requests/")
|
||||||
|
# dropdown
|
||||||
|
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
|
||||||
|
# link to requests
|
||||||
|
self.assertNotContains(portfolio_landing_page, 'href="/requests/')
|
||||||
|
# link to create
|
||||||
|
self.assertNotContains(portfolio_landing_page, 'href="/request/')
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_main_nav_when_user_has_all_permissions(self):
|
||||||
|
"""Test the nav contains a dropdown with a link to create and another link to view requests
|
||||||
|
Also test for the existence of the Create a new request btn on the requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# link to no requests
|
||||||
|
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
|
||||||
|
# dropdown
|
||||||
|
self.assertContains(portfolio_landing_page, "basic-nav-section-two")
|
||||||
|
# link to requests
|
||||||
|
self.assertContains(portfolio_landing_page, 'href="/requests/')
|
||||||
|
# link to create
|
||||||
|
self.assertContains(portfolio_landing_page, 'href="/request/')
|
||||||
|
|
||||||
|
requests_page = self.client.get(reverse("domain-requests"))
|
||||||
|
|
||||||
|
# create new request btn
|
||||||
|
self.assertContains(requests_page, "Start a new domain request")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_main_nav_when_user_has_view_but_not_edit_permissions(self):
|
||||||
|
"""Test the nav contains a simple link to view requests
|
||||||
|
Also test for the existence of the Create a new request btn on the requests page"""
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# create and submit a domain request
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
domain_request.submit()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# link to no requests
|
||||||
|
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
|
||||||
|
# dropdown
|
||||||
|
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
|
||||||
|
# link to requests
|
||||||
|
self.assertContains(portfolio_landing_page, 'href="/requests/')
|
||||||
|
# link to create
|
||||||
|
self.assertNotContains(portfolio_landing_page, 'href="/request/')
|
||||||
|
|
||||||
|
requests_page = self.client.get(reverse("domain-requests"))
|
||||||
|
|
||||||
|
# create new request btn
|
||||||
|
self.assertNotContains(requests_page, "Start a new domain request")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_organization_requests_additional_column(self):
|
||||||
|
"""The requests table has a column for created at"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home")).follow()
|
||||||
|
|
||||||
|
self.assertContains(home, "Hotel California")
|
||||||
|
self.assertContains(home, "Domain requests")
|
||||||
|
|
||||||
|
domain_requests = self.app.get(reverse("domain-requests"))
|
||||||
|
self.assertEqual(domain_requests.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(domain_requests, "Created by")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_no_org_requests_no_additional_column(self):
|
||||||
|
"""The requests table does not have a column for created at"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home"))
|
||||||
|
|
||||||
|
self.assertContains(home, "Domain requests")
|
||||||
|
self.assertNotContains(home, "Created by")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_portfolio_cache_updates_when_modified(self):
|
||||||
|
"""Test that the portfolio in session updates when the portfolio is modified"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
|
||||||
|
|
||||||
|
with override_flag("organization_feature", active=True):
|
||||||
|
# Initial request to set the portfolio in session
|
||||||
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
portfolio = self.client.session.get("portfolio")
|
||||||
|
self.assertEqual(portfolio.organization_name, "Hotel California")
|
||||||
|
self.assertContains(response, "Hotel California")
|
||||||
|
|
||||||
|
# Modify the portfolio
|
||||||
|
self.portfolio.organization_name = "Updated Hotel California"
|
||||||
|
self.portfolio.save()
|
||||||
|
|
||||||
|
# Make another request
|
||||||
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
|
||||||
|
# Check if the updated portfolio name is in the response
|
||||||
|
self.assertContains(response, "Updated Hotel California")
|
||||||
|
|
||||||
|
# Verify that the session contains the updated portfolio
|
||||||
|
portfolio = self.client.session.get("portfolio")
|
||||||
|
self.assertEqual(portfolio.organization_name, "Updated Hotel California")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_portfolio_cache_updates_when_flag_disabled_while_logged_in(self):
|
||||||
|
"""Test that the portfolio in session is set to None when the organization_feature flag is disabled"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
|
||||||
|
|
||||||
|
with override_flag("organization_feature", active=True):
|
||||||
|
# Initial request to set the portfolio in session
|
||||||
|
response = self.client.get(reverse("home"), follow=True)
|
||||||
|
portfolio = self.client.session.get("portfolio")
|
||||||
|
self.assertEqual(portfolio.organization_name, "Hotel California")
|
||||||
|
self.assertContains(response, "Hotel California")
|
||||||
|
|
||||||
|
# Disable the organization_feature flag
|
||||||
|
with override_flag("organization_feature", active=False):
|
||||||
|
# Make another request
|
||||||
|
response = self.client.get(reverse("home"))
|
||||||
|
self.assertIsNone(self.client.session.get("portfolio"))
|
||||||
|
self.assertNotContains(response, "Hotel California")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_org_user_can_delete_own_domain_request_with_permission(self):
|
||||||
|
"""Test that an org user with edit permission can delete their own DomainRequest with a deletable status."""
|
||||||
|
|
||||||
|
# Assign the user to a portfolio with edit permission
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a domain request with status WITHDRAWN
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
name="test-domain.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
)
|
||||||
|
domain_request.creator = self.user
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# Perform delete
|
||||||
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
# Check that the response is 200
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Check that the domain request no longer exists
|
||||||
|
self.assertFalse(DomainRequest.objects.filter(pk=domain_request.pk).exists())
|
||||||
|
domain_request.delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_delete_domain_request_as_org_user_without_permission_with_deletable_status(self):
|
||||||
|
"""Test that an org user without edit permission cant delete their DomainRequest even if status is deletable."""
|
||||||
|
|
||||||
|
# Assign the user to a portfolio without edit permission
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a domain request with status STARTED
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
name="test-domain.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
)
|
||||||
|
domain_request.creator = self.user
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# Attempt to delete
|
||||||
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
# Check response is 403 Forbidden
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
# Check that the domain request still exists
|
||||||
|
self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists())
|
||||||
|
domain_request.delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_org_user_cannot_delete_others_domain_requests(self):
|
||||||
|
"""Test that an org user with edit permission cannot delete DomainRequests they did not create."""
|
||||||
|
|
||||||
|
# Assign the user to a portfolio with edit permission
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create another user and a domain request
|
||||||
|
other_user = User.objects.create(username="other_user")
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
name="test-domain.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
)
|
||||||
|
domain_request.creator = other_user
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
# Perform delete as self.user
|
||||||
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
# Check response is 403 Forbidden
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
# Check that the domain request still exists
|
||||||
|
self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists())
|
||||||
|
domain_request.delete()
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock, patch
|
||||||
|
from datetime import datetime
|
||||||
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
from .common import MockSESClient, completed_domain_request # type: ignore
|
from .common import MockSESClient, completed_domain_request # type: ignore
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
from waffle.testutils import override_flag
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
DomainRequest,
|
DomainRequest,
|
||||||
|
@ -17,12 +19,14 @@ from registrar.models import (
|
||||||
User,
|
User,
|
||||||
Website,
|
Website,
|
||||||
FederalAgency,
|
FederalAgency,
|
||||||
|
Portfolio,
|
||||||
|
UserPortfolioPermission,
|
||||||
)
|
)
|
||||||
from registrar.views.domain_request import DomainRequestWizard, Step
|
from registrar.views.domain_request import DomainRequestWizard, Step
|
||||||
|
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -53,6 +57,46 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
intro_page = self.app.get(reverse("domain-request:"))
|
intro_page = self.app.get(reverse("domain-request:"))
|
||||||
self.assertContains(intro_page, "You’re about to start your .gov domain request")
|
self.assertContains(intro_page, "You’re about to start your .gov domain request")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_template_status_display(self):
|
||||||
|
"""Tests the display of status-related information in the template."""
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||||
|
domain_request.last_submitted_date = datetime.now()
|
||||||
|
domain_request.save()
|
||||||
|
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
|
self.assertContains(response, "Submitted on:")
|
||||||
|
self.assertContains(response, domain_request.last_submitted_date.strftime("%B %-d, %Y"))
|
||||||
|
|
||||||
|
@patch.object(DomainRequest, "get_first_status_set_date")
|
||||||
|
def test_get_first_status_started_date(self, mock_get_first_status_set_date):
|
||||||
|
"""Tests retrieval of the first date the status was set to 'started'."""
|
||||||
|
|
||||||
|
# Set the mock to return a fixed date
|
||||||
|
fixed_date = timezone.datetime(2023, 1, 1).date()
|
||||||
|
mock_get_first_status_set_date.return_value = fixed_date
|
||||||
|
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
|
||||||
|
domain_request.last_status_update = None
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
|
# Ensure that the date is still set to None
|
||||||
|
self.assertIsNone(domain_request.last_status_update)
|
||||||
|
print(response)
|
||||||
|
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
||||||
|
self.assertContains(response, "Started on:")
|
||||||
|
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))
|
||||||
|
|
||||||
|
# If a status date is set, we display that instead
|
||||||
|
domain_request.last_status_update = datetime.now()
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
|
|
||||||
|
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
||||||
|
self.assertContains(response, "Started on:")
|
||||||
|
self.assertContains(response, domain_request.last_status_update.strftime("%B %-d, %Y"))
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_request_form_intro_is_skipped_when_edit_access(self):
|
def test_domain_request_form_intro_is_skipped_when_edit_access(self):
|
||||||
"""Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'"""
|
"""Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'"""
|
||||||
|
@ -348,41 +392,13 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
# the domain request page
|
# the domain request page
|
||||||
self.assertEqual(purpose_result.status_code, 302)
|
self.assertEqual(purpose_result.status_code, 302)
|
||||||
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
|
self.assertEqual(purpose_result["Location"], "/request/other_contacts/")
|
||||||
num_pages_tested += 1
|
|
||||||
|
|
||||||
# ---- YOUR CONTACT INFO PAGE ----
|
|
||||||
# Follow the redirect to the next form page
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
your_contact_page = purpose_result.follow()
|
|
||||||
your_contact_form = your_contact_page.forms[0]
|
|
||||||
|
|
||||||
your_contact_form["your_contact-first_name"] = "Testy you"
|
|
||||||
your_contact_form["your_contact-last_name"] = "Tester you"
|
|
||||||
your_contact_form["your_contact-title"] = "Admin Tester"
|
|
||||||
your_contact_form["your_contact-email"] = "testy-admin@town.com"
|
|
||||||
your_contact_form["your_contact-phone"] = "(201) 555 5556"
|
|
||||||
|
|
||||||
# test next button
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
your_contact_result = your_contact_form.submit()
|
|
||||||
# validate that data from this step are being saved
|
|
||||||
domain_request = DomainRequest.objects.get() # there's only one
|
|
||||||
self.assertEqual(domain_request.submitter.first_name, "Testy you")
|
|
||||||
self.assertEqual(domain_request.submitter.last_name, "Tester you")
|
|
||||||
self.assertEqual(domain_request.submitter.title, "Admin Tester")
|
|
||||||
self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
|
|
||||||
self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
|
|
||||||
# the post request should return a redirect to the next form in
|
|
||||||
# the domain request page
|
|
||||||
self.assertEqual(your_contact_result.status_code, 302)
|
|
||||||
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
|
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
|
||||||
# ---- OTHER CONTACTS PAGE ----
|
# ---- OTHER CONTACTS PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
other_contacts_page = your_contact_result.follow()
|
other_contacts_page = purpose_result.follow()
|
||||||
|
|
||||||
# This page has 3 forms in 1.
|
# This page has 3 forms in 1.
|
||||||
# Let's set the yes/no radios to enable the other contacts fieldsets
|
# Let's set the yes/no radios to enable the other contacts fieldsets
|
||||||
|
@ -492,11 +508,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
self.assertContains(review_page, "city.gov")
|
self.assertContains(review_page, "city.gov")
|
||||||
self.assertContains(review_page, "city1.gov")
|
self.assertContains(review_page, "city1.gov")
|
||||||
self.assertContains(review_page, "For all kinds of things.")
|
self.assertContains(review_page, "For all kinds of things.")
|
||||||
self.assertContains(review_page, "Testy you")
|
|
||||||
self.assertContains(review_page, "Tester you")
|
|
||||||
self.assertContains(review_page, "Admin Tester")
|
|
||||||
self.assertContains(review_page, "testy-admin@town.com")
|
|
||||||
self.assertContains(review_page, "(201) 555-5556")
|
|
||||||
self.assertContains(review_page, "Testy2")
|
self.assertContains(review_page, "Testy2")
|
||||||
self.assertContains(review_page, "Tester2")
|
self.assertContains(review_page, "Tester2")
|
||||||
self.assertContains(review_page, "Another Tester")
|
self.assertContains(review_page, "Another Tester")
|
||||||
|
@ -704,41 +715,13 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
# the domain request page
|
# the domain request page
|
||||||
self.assertEqual(purpose_result.status_code, 302)
|
self.assertEqual(purpose_result.status_code, 302)
|
||||||
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
|
self.assertEqual(purpose_result["Location"], "/request/other_contacts/")
|
||||||
num_pages_tested += 1
|
|
||||||
|
|
||||||
# ---- YOUR CONTACT INFO PAGE ----
|
|
||||||
# Follow the redirect to the next form page
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
your_contact_page = purpose_result.follow()
|
|
||||||
your_contact_form = your_contact_page.forms[0]
|
|
||||||
|
|
||||||
your_contact_form["your_contact-first_name"] = "Testy you"
|
|
||||||
your_contact_form["your_contact-last_name"] = "Tester you"
|
|
||||||
your_contact_form["your_contact-title"] = "Admin Tester"
|
|
||||||
your_contact_form["your_contact-email"] = "testy-admin@town.com"
|
|
||||||
your_contact_form["your_contact-phone"] = "(201) 555 5556"
|
|
||||||
|
|
||||||
# test next button
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
your_contact_result = your_contact_form.submit()
|
|
||||||
# validate that data from this step are being saved
|
|
||||||
domain_request = DomainRequest.objects.get() # there's only one
|
|
||||||
self.assertEqual(domain_request.submitter.first_name, "Testy you")
|
|
||||||
self.assertEqual(domain_request.submitter.last_name, "Tester you")
|
|
||||||
self.assertEqual(domain_request.submitter.title, "Admin Tester")
|
|
||||||
self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
|
|
||||||
self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
|
|
||||||
# the post request should return a redirect to the next form in
|
|
||||||
# the domain request page
|
|
||||||
self.assertEqual(your_contact_result.status_code, 302)
|
|
||||||
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
|
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
|
||||||
# ---- OTHER CONTACTS PAGE ----
|
# ---- OTHER CONTACTS PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
other_contacts_page = your_contact_result.follow()
|
other_contacts_page = purpose_result.follow()
|
||||||
|
|
||||||
# This page has 3 forms in 1.
|
# This page has 3 forms in 1.
|
||||||
# Let's set the yes/no radios to enable the other contacts fieldsets
|
# Let's set the yes/no radios to enable the other contacts fieldsets
|
||||||
|
@ -1643,7 +1626,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
|
@ -1780,7 +1762,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
|
@ -1855,7 +1836,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
|
@ -1933,7 +1913,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
|
@ -2010,7 +1989,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
|
@ -2086,7 +2064,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
|
@ -2271,22 +2248,13 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
self.assertEquals("Testy2", senior_official.first_name)
|
self.assertEquals("Testy2", senior_official.first_name)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_edit_submitter_in_place(self):
|
def test_edit_creator_in_place(self):
|
||||||
"""When you:
|
"""When you:
|
||||||
1. edit a submitter (your contact) which is not joined to another model,
|
1. edit a your user profile information,
|
||||||
2. then submit,
|
2. then submit,
|
||||||
the domain request is linked to the existing submitter, and the submitter updated."""
|
the domain request also updates its creator data to reflect user profile changes."""
|
||||||
|
|
||||||
# Populate the database with a domain request that
|
# Populate the database with a domain request
|
||||||
# has a submitter
|
|
||||||
# We'll do it from scratch
|
|
||||||
you, _ = Contact.objects.get_or_create(
|
|
||||||
first_name="Testy",
|
|
||||||
last_name="Tester",
|
|
||||||
title="Chief Tester",
|
|
||||||
email="testy@town.com",
|
|
||||||
phone="(201) 555 5555",
|
|
||||||
)
|
|
||||||
domain_request, _ = DomainRequest.objects.get_or_create(
|
domain_request, _ = DomainRequest.objects.get_or_create(
|
||||||
generic_org_type="federal",
|
generic_org_type="federal",
|
||||||
federal_type="executive",
|
federal_type="executive",
|
||||||
|
@ -2297,14 +2265,11 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
address_line1="address 1",
|
address_line1="address 1",
|
||||||
state_territory="NY",
|
state_territory="NY",
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
|
|
||||||
# submitter_pk is the initial pk of the submitter. set it before update
|
creator_pk = self.user.id
|
||||||
# to be able to verify after update that the same contact object is in place
|
|
||||||
submitter_pk = you.id
|
|
||||||
|
|
||||||
# prime the form by visiting /edit
|
# prime the form by visiting /edit
|
||||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
@ -2315,98 +2280,25 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
your_contact_page = self.app.get(reverse("domain-request:your_contact"))
|
profile_page = self.app.get("/user-profile")
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
your_contact_form = your_contact_page.forms[0]
|
profile_form = profile_page.forms[0]
|
||||||
|
|
||||||
# Minimal check to ensure the form is loaded
|
# Minimal check to ensure the form is loaded
|
||||||
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
|
self.assertEqual(profile_form["first_name"].value, self.user.first_name)
|
||||||
|
|
||||||
# update the first name of the contact
|
# update the first name of the contact
|
||||||
your_contact_form["your_contact-first_name"] = "Testy2"
|
profile_form["first_name"] = "Testy2"
|
||||||
|
|
||||||
# Submit the updated form
|
# Submit the updated form
|
||||||
your_contact_form.submit()
|
profile_form.submit()
|
||||||
|
|
||||||
domain_request.refresh_from_db()
|
domain_request.refresh_from_db()
|
||||||
|
|
||||||
updated_submitter = domain_request.submitter
|
updated_creator = domain_request.creator
|
||||||
self.assertEquals(submitter_pk, updated_submitter.id)
|
self.assertEquals(creator_pk, updated_creator.id)
|
||||||
self.assertEquals("Testy2", updated_submitter.first_name)
|
self.assertEquals("Testy2", updated_creator.first_name)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_edit_submitter_creates_new(self):
|
|
||||||
"""When you:
|
|
||||||
1. edit an existing your contact which IS joined to another model,
|
|
||||||
2. then submit,
|
|
||||||
the domain request is linked to a new Contact, and the new Contact is updated."""
|
|
||||||
|
|
||||||
# Populate the database with a domain request that
|
|
||||||
# has submitter assigned to it, the submitter is also
|
|
||||||
# an other contact initially
|
|
||||||
# We'll do it from scratch
|
|
||||||
submitter, _ = Contact.objects.get_or_create(
|
|
||||||
first_name="Testy",
|
|
||||||
last_name="Tester",
|
|
||||||
title="Chief Tester",
|
|
||||||
email="testy@town.com",
|
|
||||||
phone="(201) 555 5555",
|
|
||||||
)
|
|
||||||
domain_request, _ = DomainRequest.objects.get_or_create(
|
|
||||||
generic_org_type="federal",
|
|
||||||
federal_type="executive",
|
|
||||||
purpose="Purpose of the site",
|
|
||||||
anything_else="No",
|
|
||||||
is_policy_acknowledged=True,
|
|
||||||
organization_name="Testorg",
|
|
||||||
address_line1="address 1",
|
|
||||||
state_territory="NY",
|
|
||||||
zipcode="10002",
|
|
||||||
submitter=submitter,
|
|
||||||
creator=self.user,
|
|
||||||
status="started",
|
|
||||||
)
|
|
||||||
domain_request.other_contacts.add(submitter)
|
|
||||||
|
|
||||||
# submitter_pk is the initial pk of the your contact. set it before update
|
|
||||||
# to be able to verify after update that the other contact is still in place
|
|
||||||
# and not updated, and that the new submitter has a new id
|
|
||||||
submitter_pk = submitter.id
|
|
||||||
|
|
||||||
# prime the form by visiting /edit
|
|
||||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
|
||||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
|
||||||
# resetting the session key on each new request, thus destroying the concept
|
|
||||||
# of a "session". We are going to do it manually, saving the session ID here
|
|
||||||
# and then setting the cookie on each request.
|
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
|
|
||||||
your_contact_page = self.app.get(reverse("domain-request:your_contact"))
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
|
|
||||||
your_contact_form = your_contact_page.forms[0]
|
|
||||||
|
|
||||||
# Minimal check to ensure the form is loaded
|
|
||||||
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
|
|
||||||
|
|
||||||
# update the first name of the contact
|
|
||||||
your_contact_form["your_contact-first_name"] = "Testy2"
|
|
||||||
|
|
||||||
# Submit the updated form
|
|
||||||
your_contact_form.submit()
|
|
||||||
|
|
||||||
domain_request.refresh_from_db()
|
|
||||||
|
|
||||||
# assert that the other contact is not updated
|
|
||||||
other_contacts = domain_request.other_contacts.all()
|
|
||||||
other_contact = other_contacts[0]
|
|
||||||
self.assertEquals(submitter_pk, other_contact.id)
|
|
||||||
self.assertEquals("Testy", other_contact.first_name)
|
|
||||||
# assert that the submitter is updated
|
|
||||||
submitter = domain_request.submitter
|
|
||||||
self.assertEquals("Testy2", submitter.first_name)
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_request_about_your_organiztion_interstate(self):
|
def test_domain_request_about_your_organiztion_interstate(self):
|
||||||
|
@ -2729,7 +2621,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
zipcode="10002",
|
zipcode="10002",
|
||||||
senior_official=so,
|
senior_official=so,
|
||||||
requested_domain=domain,
|
requested_domain=domain,
|
||||||
submitter=you,
|
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
)
|
)
|
||||||
domain_request.other_contacts.add(other)
|
domain_request.other_contacts.add(other)
|
||||||
|
@ -2874,7 +2765,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
self.assertContains(detail_page, "city1.gov")
|
self.assertContains(detail_page, "city1.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
self.assertContains(detail_page, "testy@town.com")
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
self.assertContains(detail_page, "Admin Tester")
|
|
||||||
self.assertContains(detail_page, "Status:")
|
self.assertContains(detail_page, "Status:")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -2891,7 +2781,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
self.assertContains(detail_page, "city.gov")
|
self.assertContains(detail_page, "city.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
self.assertContains(detail_page, "testy@town.com")
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
self.assertContains(detail_page, "Admin Tester")
|
|
||||||
self.assertContains(detail_page, "Status:")
|
self.assertContains(detail_page, "Status:")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -2905,7 +2794,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
self.assertContains(detail_page, "city1.gov")
|
self.assertContains(detail_page, "city1.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
self.assertContains(detail_page, "testy@town.com")
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
self.assertContains(detail_page, "Admin Tester")
|
|
||||||
self.assertContains(detail_page, "Status:")
|
self.assertContains(detail_page, "Status:")
|
||||||
# click the "Withdraw request" button
|
# click the "Withdraw request" button
|
||||||
mock_client = MockSESClient()
|
mock_client = MockSESClient()
|
||||||
|
@ -2925,6 +2813,38 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
response = self.client.get("/get-domain-requests-json/")
|
response = self.client.get("/get-domain-requests-json/")
|
||||||
self.assertContains(response, "Withdrawn")
|
self.assertContains(response, "Withdrawn")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_domain_request_withdraw_portfolio_redirects_correctly(self):
|
||||||
|
"""Tests that the withdraw button on portfolio redirects to the portfolio domain requests page"""
|
||||||
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
|
self.assertContains(detail_page, "city.gov")
|
||||||
|
self.assertContains(detail_page, "city1.gov")
|
||||||
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
|
self.assertContains(detail_page, "Status:")
|
||||||
|
# click the "Withdraw request" button
|
||||||
|
mock_client = MockSESClient()
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
with less_console_noise():
|
||||||
|
withdraw_page = detail_page.click("Withdraw request")
|
||||||
|
self.assertContains(withdraw_page, "Withdraw request for")
|
||||||
|
home_page = withdraw_page.click("Withdraw request")
|
||||||
|
|
||||||
|
# Assert that it redirects to the portfolio requests page and the status has been updated to withdrawn
|
||||||
|
self.assertEqual(home_page.status_code, 302)
|
||||||
|
self.assertEqual(home_page.location, reverse("domain-requests"))
|
||||||
|
|
||||||
|
response = self.client.get("/get-domain-requests-json/")
|
||||||
|
self.assertContains(response, "Withdrawn")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_request_withdraw_no_permissions(self):
|
def test_domain_request_withdraw_no_permissions(self):
|
||||||
"""Can't withdraw domain requests as a restricted user."""
|
"""Can't withdraw domain requests as a restricted user."""
|
||||||
|
@ -2938,7 +2858,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
self.assertContains(detail_page, "city1.gov")
|
self.assertContains(detail_page, "city1.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
self.assertContains(detail_page, "testy@town.com")
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
self.assertContains(detail_page, "Admin Tester")
|
|
||||||
self.assertContains(detail_page, "Status:")
|
self.assertContains(detail_page, "Status:")
|
||||||
# Restricted user trying to withdraw results in 403 error
|
# Restricted user trying to withdraw results in 403 error
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
|
@ -3037,10 +2956,10 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
self.assertEqual(detail_page.status_code, 200)
|
self.assertEqual(detail_page.status_code, 200)
|
||||||
|
|
||||||
# 10 unlocked steps, one active step, the review step will have link_usa but not check_circle
|
# 10 unlocked steps, one active step, the review step will have link_usa but not check_circle
|
||||||
self.assertContains(detail_page, "#check_circle", count=10)
|
self.assertContains(detail_page, "#check_circle", count=9)
|
||||||
# Type of organization
|
# Type of organization
|
||||||
self.assertContains(detail_page, "usa-current", count=1)
|
self.assertContains(detail_page, "usa-current", count=1)
|
||||||
self.assertContains(detail_page, "link_usa-checked", count=11)
|
self.assertContains(detail_page, "link_usa-checked", count=10)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.fail(f"Expected a redirect, but got a different response: {response}")
|
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||||
|
@ -3072,7 +2991,6 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
requested_domain=site,
|
requested_domain=site,
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
senior_official=contact,
|
senior_official=contact,
|
||||||
submitter=contact_user,
|
|
||||||
)
|
)
|
||||||
domain_request.other_contacts.set([contact_2])
|
domain_request.other_contacts.set([contact_2])
|
||||||
|
|
||||||
|
@ -3098,12 +3016,12 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
# Now 'detail_page' contains the response after following the redirect
|
# Now 'detail_page' contains the response after following the redirect
|
||||||
self.assertEqual(detail_page.status_code, 200)
|
self.assertEqual(detail_page.status_code, 200)
|
||||||
|
|
||||||
# 5 unlocked steps (so, domain, submitter, other contacts, and current sites
|
# 5 unlocked steps (so, domain, other contacts, and current sites
|
||||||
# which unlocks if domain exists), one active step, the review step is locked
|
# which unlocks if domain exists), one active step, the review step is locked
|
||||||
self.assertContains(detail_page, "#check_circle", count=5)
|
self.assertContains(detail_page, "#check_circle", count=4)
|
||||||
# Type of organization
|
# Type of organization
|
||||||
self.assertContains(detail_page, "usa-current", count=1)
|
self.assertContains(detail_page, "usa-current", count=1)
|
||||||
self.assertContains(detail_page, "link_usa-checked", count=5)
|
self.assertContains(detail_page, "link_usa-checked", count=4)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.fail(f"Expected a redirect, but got a different response: {response}")
|
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||||
|
|
|
@ -2,9 +2,14 @@ from registrar.models import DomainRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from registrar.models.draft_domain import DraftDomain
|
from registrar.models.draft_domain import DraftDomain
|
||||||
|
from registrar.models.portfolio import Portfolio
|
||||||
|
from registrar.models.user import User
|
||||||
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
|
from waffle.testutils import override_flag
|
||||||
|
|
||||||
|
|
||||||
class GetRequestsJsonTest(TestWithUser, WebTest):
|
class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
|
@ -20,6 +25,19 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
||||||
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
||||||
|
|
||||||
|
# Create Portfolio
|
||||||
|
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Example org")
|
||||||
|
|
||||||
|
# create a second user to assign requests to
|
||||||
|
cls.user2 = User.objects.create(
|
||||||
|
username="test_user2",
|
||||||
|
first_name="Second",
|
||||||
|
last_name="last",
|
||||||
|
email="info2@example.com",
|
||||||
|
phone="8003111234",
|
||||||
|
title="title",
|
||||||
|
)
|
||||||
|
|
||||||
# Create domain requests for the user
|
# Create domain requests for the user
|
||||||
cls.domain_requests = [
|
cls.domain_requests = [
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
|
@ -28,6 +46,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
last_submitted_date="2024-01-01",
|
last_submitted_date="2024-01-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-01-01",
|
created_at="2024-01-01",
|
||||||
|
portfolio=cls.portfolio,
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=cls.user,
|
creator=cls.user,
|
||||||
|
@ -42,6 +61,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
last_submitted_date="2024-03-01",
|
last_submitted_date="2024-03-01",
|
||||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
created_at="2024-03-01",
|
created_at="2024-03-01",
|
||||||
|
portfolio=cls.portfolio,
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=cls.user,
|
creator=cls.user,
|
||||||
|
@ -113,6 +133,14 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED,
|
status=DomainRequest.DomainRequestStatus.APPROVED,
|
||||||
created_at="2024-12-01",
|
created_at="2024-12-01",
|
||||||
),
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=cls.user2,
|
||||||
|
requested_domain=None,
|
||||||
|
last_submitted_date="2024-12-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
created_at="2024-12-01",
|
||||||
|
portfolio=cls.portfolio,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -120,6 +148,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
DraftDomain.objects.all().delete()
|
DraftDomain.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
|
||||||
def test_get_domain_requests_json_authenticated(self):
|
def test_get_domain_requests_json_authenticated(self):
|
||||||
"""Test that domain requests are returned properly for an authenticated user."""
|
"""Test that domain requests are returned properly for an authenticated user."""
|
||||||
|
@ -262,6 +291,118 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
for expected_value, actual_value in zip(expected_domain_values, requested_domains):
|
for expected_value, actual_value in zip(expected_domain_values, requested_domains):
|
||||||
self.assertEqual(expected_value, actual_value)
|
self.assertEqual(expected_value, actual_value)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_get_domain_requests_json_with_portfolio_view_all_requests(self):
|
||||||
|
"""Test that an authenticated user gets the list of 3 requests for portfolio. The 3 requests
|
||||||
|
are the requests that are associated with the portfolio."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
|
# Check the number of requests
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 3)
|
||||||
|
|
||||||
|
# Expected domain requests
|
||||||
|
expected_domain_requests = [self.domain_requests[0], self.domain_requests[2], self.domain_requests[13]]
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]]
|
||||||
|
requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]]
|
||||||
|
creator = [domain_request["creator"] for domain_request in data["domain_requests"]]
|
||||||
|
status = [domain_request["status"] for domain_request in data["domain_requests"]]
|
||||||
|
action_urls = [domain_request["action_url"] for domain_request in data["domain_requests"]]
|
||||||
|
action_labels = [domain_request["action_label"] for domain_request in data["domain_requests"]]
|
||||||
|
svg_icons = [domain_request["svg_icon"] for domain_request in data["domain_requests"]]
|
||||||
|
|
||||||
|
# Check fields for each domain_request
|
||||||
|
for i, expected_domain_request in enumerate(expected_domain_requests):
|
||||||
|
self.assertEqual(expected_domain_request.id, domain_request_ids[i])
|
||||||
|
if expected_domain_request.requested_domain:
|
||||||
|
self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i])
|
||||||
|
else:
|
||||||
|
self.assertIsNone(requested_domain[i])
|
||||||
|
self.assertEqual(expected_domain_request.creator.email, creator[i])
|
||||||
|
# Check action url, action label and svg icon
|
||||||
|
# Example domain requests will test each of below three scenarios
|
||||||
|
if creator[i] != self.user.email:
|
||||||
|
# Test case where action is View
|
||||||
|
self.assertEqual("View", action_labels[i])
|
||||||
|
self.assertEqual("#", action_urls[i])
|
||||||
|
self.assertEqual("visibility", svg_icons[i])
|
||||||
|
elif status[i] in [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED.label,
|
||||||
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED.label,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN.label,
|
||||||
|
]:
|
||||||
|
# Test case where action is Edit
|
||||||
|
self.assertEqual("Edit", action_labels[i])
|
||||||
|
self.assertEqual(
|
||||||
|
reverse("edit-domain-request", kwargs={"id": expected_domain_request.id}), action_urls[i]
|
||||||
|
)
|
||||||
|
self.assertEqual("edit", svg_icons[i])
|
||||||
|
else:
|
||||||
|
# Test case where action is Manage
|
||||||
|
self.assertEqual("Manage", action_labels[i])
|
||||||
|
self.assertEqual(
|
||||||
|
reverse("domain-request-status", kwargs={"pk": expected_domain_request.id}), action_urls[i]
|
||||||
|
)
|
||||||
|
self.assertEqual("settings", svg_icons[i])
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_get_domain_requests_json_with_portfolio_edit_requests(self):
|
||||||
|
"""Test that an authenticated user gets the list of 2 requests for portfolio. The 2 requests
|
||||||
|
are the requests that are associated with the portfolio and owned by self.user."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
|
# Check the number of requests
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 2)
|
||||||
|
|
||||||
|
# Expected domain requests
|
||||||
|
expected_domain_requests = [self.domain_requests[0], self.domain_requests[2]]
|
||||||
|
|
||||||
|
# Extract fields from response, since other tests test all fields, only ids and requested
|
||||||
|
# domains tested in this test
|
||||||
|
domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]]
|
||||||
|
requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]]
|
||||||
|
|
||||||
|
# Check fields for each domain_request
|
||||||
|
for i, expected_domain_request in enumerate(expected_domain_requests):
|
||||||
|
self.assertEqual(expected_domain_request.id, domain_request_ids[i])
|
||||||
|
if expected_domain_request.requested_domain:
|
||||||
|
self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i])
|
||||||
|
else:
|
||||||
|
self.assertIsNone(requested_domain[i])
|
||||||
|
|
||||||
def test_pagination(self):
|
def test_pagination(self):
|
||||||
"""Test that pagination works properly. There are 11 total non-approved requests and
|
"""Test that pagination works properly. There are 11 total non-approved requests and
|
||||||
a page size of 10"""
|
a page size of 10"""
|
||||||
|
@ -317,3 +458,81 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
# Ensure no approved requests are included
|
# Ensure no approved requests are included
|
||||||
for domain_request in data["domain_requests"]:
|
for domain_request in data["domain_requests"]:
|
||||||
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
"""Tests our search functionality. We expect that search filters on creator only when we are in a portfolio"""
|
||||||
|
# Test search for domain name
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "lamb"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 1)
|
||||||
|
|
||||||
|
requested_domain = data["domain_requests"][0]["requested_domain"]
|
||||||
|
self.assertEqual(requested_domain, "lamb-chops.gov")
|
||||||
|
|
||||||
|
# Test search for 'New domain request'
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "new domain"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(any(req["requested_domain"] is None for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test search with portfolio (including creator search)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
with override_flag("organization_feature", active=True), override_flag("organization_requests", active=True):
|
||||||
|
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
)
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_domain_requests_json"), {"search_term": "info", "portfolio": self.portfolio.id}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(any(req["creator"].startswith("info") for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test search without portfolio (should not search on creator)
|
||||||
|
with override_flag("organization_feature", active=False), override_flag("organization_requests", active=False):
|
||||||
|
user_perm.delete()
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "info"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 0)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_status_filter(self):
|
||||||
|
"""Test that status filtering works properly"""
|
||||||
|
# Test a single status
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"status": "started"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test an invalid status
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"status": "approved"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 0)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_combined_filtering_and_sorting(self):
|
||||||
|
"""Test that combining filters and sorting works properly"""
|
||||||
|
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_domain_requests_json"),
|
||||||
|
{"search_term": "beef", "status": "started", "portfolio": self.portfolio.id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(all("beef" in req["requested_domain"] for req in data["domain_requests"]))
|
||||||
|
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||||
|
created_at_dates = [req["created_at"] for req in data["domain_requests"]]
|
||||||
|
self.assertEqual(created_at_dates, sorted(created_at_dates, reverse=True))
|
||||||
|
user_perm.delete()
|
||||||
|
|
|
@ -77,17 +77,15 @@ class FSMErrorCodes(IntEnum):
|
||||||
- 1 APPROVE_DOMAIN_IN_USE The domain is already in use
|
- 1 APPROVE_DOMAIN_IN_USE The domain is already in use
|
||||||
- 2 NO_INVESTIGATOR No investigator is assigned
|
- 2 NO_INVESTIGATOR No investigator is assigned
|
||||||
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
|
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
|
||||||
- 4 INVESTIGATOR_NOT_SUBMITTER The form submitter is not the investigator
|
- 4 NO_REJECTION_REASON No rejection reason is specified
|
||||||
- 5 NO_REJECTION_REASON No rejection reason is specified
|
- 5 NO_ACTION_NEEDED_REASON No action needed reason is specified
|
||||||
- 6 NO_ACTION_NEEDED_REASON No action needed reason is specified
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
APPROVE_DOMAIN_IN_USE = 1
|
APPROVE_DOMAIN_IN_USE = 1
|
||||||
NO_INVESTIGATOR = 2
|
NO_INVESTIGATOR = 2
|
||||||
INVESTIGATOR_NOT_STAFF = 3
|
INVESTIGATOR_NOT_STAFF = 3
|
||||||
INVESTIGATOR_NOT_SUBMITTER = 4
|
NO_REJECTION_REASON = 4
|
||||||
NO_REJECTION_REASON = 5
|
NO_ACTION_NEEDED_REASON = 5
|
||||||
NO_ACTION_NEEDED_REASON = 6
|
|
||||||
|
|
||||||
|
|
||||||
class FSMDomainRequestError(Exception):
|
class FSMDomainRequestError(Exception):
|
||||||
|
@ -100,7 +98,6 @@ class FSMDomainRequestError(Exception):
|
||||||
FSMErrorCodes.APPROVE_DOMAIN_IN_USE: ("Cannot approve. Requested domain is already in use."),
|
FSMErrorCodes.APPROVE_DOMAIN_IN_USE: ("Cannot approve. Requested domain is already in use."),
|
||||||
FSMErrorCodes.NO_INVESTIGATOR: ("Investigator is required for this status."),
|
FSMErrorCodes.NO_INVESTIGATOR: ("Investigator is required for this status."),
|
||||||
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
|
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
|
||||||
FSMErrorCodes.INVESTIGATOR_NOT_SUBMITTER: ("Only the assigned investigator can make this change."),
|
|
||||||
FSMErrorCodes.NO_REJECTION_REASON: ("A reason is required for this status."),
|
FSMErrorCodes.NO_REJECTION_REASON: ("A reason is required for this status."),
|
||||||
FSMErrorCodes.NO_ACTION_NEEDED_REASON: ("A reason is required for this status."),
|
FSMErrorCodes.NO_ACTION_NEEDED_REASON: ("A reason is required for this status."),
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ from .domain import (
|
||||||
DomainNameserversView,
|
DomainNameserversView,
|
||||||
DomainDNSSECView,
|
DomainDNSSECView,
|
||||||
DomainDsDataView,
|
DomainDsDataView,
|
||||||
DomainYourContactInformationView,
|
|
||||||
DomainSecurityEmailView,
|
DomainSecurityEmailView,
|
||||||
DomainUsersView,
|
DomainUsersView,
|
||||||
DomainAddUserView,
|
DomainAddUserView,
|
||||||
|
|
|
@ -41,7 +41,6 @@ from registrar.models.utility.contact_error import ContactError
|
||||||
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
||||||
|
|
||||||
from ..forms import (
|
from ..forms import (
|
||||||
UserForm,
|
|
||||||
SeniorOfficialContactForm,
|
SeniorOfficialContactForm,
|
||||||
DomainOrgNameAddressForm,
|
DomainOrgNameAddressForm,
|
||||||
DomainAddUserForm,
|
DomainAddUserForm,
|
||||||
|
@ -60,7 +59,6 @@ from epplibwrapper import (
|
||||||
|
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
||||||
from waffle.decorators import waffle_flag
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -240,7 +238,7 @@ class DomainView(DomainBaseView):
|
||||||
If particular views allow permissions, they will need to override
|
If particular views allow permissions, they will need to override
|
||||||
this function."""
|
this function."""
|
||||||
portfolio = self.request.session.get("portfolio")
|
portfolio = self.request.session.get("portfolio")
|
||||||
if self.request.user.has_domains_portfolio_permission(portfolio):
|
if self.request.user.has_any_domains_portfolio_permission(portfolio):
|
||||||
if Domain.objects.filter(id=pk).exists():
|
if Domain.objects.filter(id=pk).exists():
|
||||||
domain = Domain.objects.get(id=pk)
|
domain = Domain.objects.get(id=pk)
|
||||||
if domain.domain_info.portfolio == portfolio:
|
if domain.domain_info.portfolio == portfolio:
|
||||||
|
@ -271,7 +269,7 @@ class DomainView(DomainBaseView):
|
||||||
|
|
||||||
|
|
||||||
class DomainOrgNameAddressView(DomainFormBaseView):
|
class DomainOrgNameAddressView(DomainFormBaseView):
|
||||||
"""Organization name and mailing address view"""
|
"""Organization view"""
|
||||||
|
|
||||||
model = Domain
|
model = Domain
|
||||||
template_name = "domain_org_name_address.html"
|
template_name = "domain_org_name_address.html"
|
||||||
|
@ -749,38 +747,6 @@ class DomainDsDataView(DomainFormBaseView):
|
||||||
return super().form_valid(formset)
|
return super().form_valid(formset)
|
||||||
|
|
||||||
|
|
||||||
class DomainYourContactInformationView(DomainFormBaseView):
|
|
||||||
"""Domain your contact information editing view."""
|
|
||||||
|
|
||||||
template_name = "domain_your_contact_information.html"
|
|
||||||
form_class = UserForm
|
|
||||||
|
|
||||||
@waffle_flag("!profile_feature") # type: ignore
|
|
||||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_form_kwargs(self, *args, **kwargs):
|
|
||||||
"""Add domain_info.submitter instance to make a bound form."""
|
|
||||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
|
||||||
form_kwargs["instance"] = self.request.user
|
|
||||||
return form_kwargs
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
"""Redirect to the your contact information for the domain."""
|
|
||||||
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
"""The form is valid, call setter in model."""
|
|
||||||
|
|
||||||
# Post to DB using values from the form
|
|
||||||
form.save()
|
|
||||||
|
|
||||||
messages.success(self.request, "Your contact information for all your domains has been updated.")
|
|
||||||
|
|
||||||
# superclass has the redirect
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class DomainSecurityEmailView(DomainFormBaseView):
|
class DomainSecurityEmailView(DomainFormBaseView):
|
||||||
"""Domain security email editing view."""
|
"""Domain security email editing view."""
|
||||||
|
|
||||||
|
@ -955,6 +921,23 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Check to see if an invite has already been sent
|
||||||
|
try:
|
||||||
|
invite = DomainInvitation.objects.get(email=email, domain=self.object)
|
||||||
|
# check if the invite has already been accepted
|
||||||
|
if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
|
||||||
|
add_success = False
|
||||||
|
messages.warning(
|
||||||
|
self.request,
|
||||||
|
f"{email} is already a manager for this domain.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
add_success = False
|
||||||
|
# else if it has been sent but not accepted
|
||||||
|
messages.warning(self.request, f"{email} has already been invited to this domain")
|
||||||
|
except Exception:
|
||||||
|
logger.error("An error occured")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
"emails/domain_invitation.txt",
|
"emails/domain_invitation.txt",
|
||||||
|
@ -980,24 +963,13 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
|
|
||||||
def _make_invitation(self, email_address: str, requestor: User):
|
def _make_invitation(self, email_address: str, requestor: User):
|
||||||
"""Make a Domain invitation for this email and redirect with a message."""
|
"""Make a Domain invitation for this email and redirect with a message."""
|
||||||
# Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.)
|
|
||||||
try:
|
try:
|
||||||
invite = DomainInvitation.objects.get(email=email_address, domain=self.object)
|
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
||||||
# that invitation already existed
|
except EmailSendingError:
|
||||||
if invite is not None:
|
messages.warning(self.request, "Could not send email invitation.")
|
||||||
messages.warning(
|
else:
|
||||||
self.request,
|
# (NOTE: only create a domainInvitation if the e-mail sends correctly)
|
||||||
f"{email_address} has already been invited to this domain.",
|
DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||||
)
|
|
||||||
except DomainInvitation.DoesNotExist:
|
|
||||||
# Try to send the invitation. If it succeeds, add it to the DomainInvitation table.
|
|
||||||
try:
|
|
||||||
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
|
||||||
except EmailSendingError:
|
|
||||||
messages.warning(self.request, "Could not send email invitation.")
|
|
||||||
else:
|
|
||||||
# (NOTE: only create a domainInvitation if the e-mail sends correctly)
|
|
||||||
DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
@ -1037,11 +1009,9 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
role=UserDomainRole.Roles.MANAGER,
|
role=UserDomainRole.Roles.MANAGER,
|
||||||
)
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# User already has the desired role! Do nothing??
|
messages.warning(self.request, f"{requested_email} is already a manager for this domain")
|
||||||
pass
|
else:
|
||||||
|
messages.success(self.request, f"Added user {requested_email}.")
|
||||||
messages.success(self.request, f"Added user {requested_email}.")
|
|
||||||
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,6 @@ from .utility import (
|
||||||
DomainRequestWizardPermissionView,
|
DomainRequestWizardPermissionView,
|
||||||
)
|
)
|
||||||
|
|
||||||
from waffle.decorators import flag_is_active, waffle_flag
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +43,6 @@ class Step(StrEnum):
|
||||||
CURRENT_SITES = "current_sites"
|
CURRENT_SITES = "current_sites"
|
||||||
DOTGOV_DOMAIN = "dotgov_domain"
|
DOTGOV_DOMAIN = "dotgov_domain"
|
||||||
PURPOSE = "purpose"
|
PURPOSE = "purpose"
|
||||||
YOUR_CONTACT = "your_contact"
|
|
||||||
OTHER_CONTACTS = "other_contacts"
|
OTHER_CONTACTS = "other_contacts"
|
||||||
ADDITIONAL_DETAILS = "additional_details"
|
ADDITIONAL_DETAILS = "additional_details"
|
||||||
REQUIREMENTS = "requirements"
|
REQUIREMENTS = "requirements"
|
||||||
|
@ -85,13 +82,12 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
Step.TRIBAL_GOVERNMENT: _("Tribal government"),
|
Step.TRIBAL_GOVERNMENT: _("Tribal government"),
|
||||||
Step.ORGANIZATION_FEDERAL: _("Federal government branch"),
|
Step.ORGANIZATION_FEDERAL: _("Federal government branch"),
|
||||||
Step.ORGANIZATION_ELECTION: _("Election office"),
|
Step.ORGANIZATION_ELECTION: _("Election office"),
|
||||||
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
|
Step.ORGANIZATION_CONTACT: _("Organization"),
|
||||||
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
||||||
Step.SENIOR_OFFICIAL: _("Senior official"),
|
Step.SENIOR_OFFICIAL: _("Senior official"),
|
||||||
Step.CURRENT_SITES: _("Current websites"),
|
Step.CURRENT_SITES: _("Current websites"),
|
||||||
Step.DOTGOV_DOMAIN: _(".gov domain"),
|
Step.DOTGOV_DOMAIN: _(".gov domain"),
|
||||||
Step.PURPOSE: _("Purpose of your domain"),
|
Step.PURPOSE: _("Purpose of your domain"),
|
||||||
Step.YOUR_CONTACT: _("Your contact information"),
|
|
||||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||||
Step.ADDITIONAL_DETAILS: _("Additional details"),
|
Step.ADDITIONAL_DETAILS: _("Additional details"),
|
||||||
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||||
|
@ -152,7 +148,14 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
except DomainRequest.DoesNotExist:
|
except DomainRequest.DoesNotExist:
|
||||||
logger.debug("DomainRequest id %s did not have a DomainRequest" % id)
|
logger.debug("DomainRequest id %s did not have a DomainRequest" % id)
|
||||||
|
|
||||||
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
# If a user is creating a request, we assume that perms are handled upstream
|
||||||
|
if self.request.user.is_org_user(self.request):
|
||||||
|
self._domain_request = DomainRequest.objects.create(
|
||||||
|
creator=self.request.user,
|
||||||
|
portfolio=self.request.session.get("portfolio"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
||||||
|
|
||||||
self.storage["domain_request_id"] = self._domain_request.id
|
self.storage["domain_request_id"] = self._domain_request.id
|
||||||
return self._domain_request
|
return self._domain_request
|
||||||
|
@ -375,7 +378,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
),
|
),
|
||||||
"dotgov_domain": self.domain_request.requested_domain is not None,
|
"dotgov_domain": self.domain_request.requested_domain is not None,
|
||||||
"purpose": self.domain_request.purpose is not None,
|
"purpose": self.domain_request.purpose is not None,
|
||||||
"your_contact": self.domain_request.submitter is not None,
|
|
||||||
"other_contacts": (
|
"other_contacts": (
|
||||||
self.domain_request.other_contacts.exists()
|
self.domain_request.other_contacts.exists()
|
||||||
or self.domain_request.no_other_contacts_rationale is not None
|
or self.domain_request.no_other_contacts_rationale is not None
|
||||||
|
@ -395,6 +397,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
"""Define context for access on all wizard pages."""
|
"""Define context for access on all wizard pages."""
|
||||||
|
|
||||||
|
requested_domain_name = None
|
||||||
|
if self.domain_request.requested_domain is not None:
|
||||||
|
requested_domain_name = self.domain_request.requested_domain.name
|
||||||
|
|
||||||
context_stuff = {}
|
context_stuff = {}
|
||||||
if DomainRequest._form_complete(self.domain_request, self.request):
|
if DomainRequest._form_complete(self.domain_request, self.request):
|
||||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
||||||
|
@ -411,6 +417,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
You’ll only be able to withdraw your request.",
|
You’ll only be able to withdraw your request.",
|
||||||
"review_form_is_complete": True,
|
"review_form_is_complete": True,
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
|
"requested_domain__name": requested_domain_name,
|
||||||
}
|
}
|
||||||
else: # form is not complete
|
else: # form is not complete
|
||||||
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
||||||
|
@ -426,6 +433,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
Return to the request and visit the steps that are marked as "incomplete."',
|
Return to the request and visit the steps that are marked as "incomplete."',
|
||||||
"review_form_is_complete": False,
|
"review_form_is_complete": False,
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
|
"requested_domain__name": requested_domain_name,
|
||||||
}
|
}
|
||||||
return context_stuff
|
return context_stuff
|
||||||
|
|
||||||
|
@ -439,9 +447,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
if condition:
|
if condition:
|
||||||
step_list.append(step)
|
step_list.append(step)
|
||||||
|
|
||||||
if flag_is_active(self.request, "profile_feature"):
|
|
||||||
step_list.remove(Step.YOUR_CONTACT)
|
|
||||||
|
|
||||||
return step_list
|
return step_list
|
||||||
|
|
||||||
def goto(self, step):
|
def goto(self, step):
|
||||||
|
@ -505,7 +510,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
# if user opted to save progress and return,
|
# if user opted to save progress and return,
|
||||||
# return them to the home page
|
# return them to the home page
|
||||||
if button == "save_and_return":
|
if button == "save_and_return":
|
||||||
return HttpResponseRedirect(reverse("home"))
|
if request.user.is_org_user(request):
|
||||||
|
return HttpResponseRedirect(reverse("domain-requests"))
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse("home"))
|
||||||
|
|
||||||
# otherwise, proceed as normal
|
# otherwise, proceed as normal
|
||||||
return self.goto_next_step()
|
return self.goto_next_step()
|
||||||
|
|
||||||
|
@ -582,15 +591,6 @@ class Purpose(DomainRequestWizard):
|
||||||
forms = [forms.PurposeForm]
|
forms = [forms.PurposeForm]
|
||||||
|
|
||||||
|
|
||||||
class YourContact(DomainRequestWizard):
|
|
||||||
template_name = "domain_request_your_contact.html"
|
|
||||||
forms = [forms.YourContactForm]
|
|
||||||
|
|
||||||
@waffle_flag("!profile_feature") # type: ignore
|
|
||||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class OtherContacts(DomainRequestWizard):
|
class OtherContacts(DomainRequestWizard):
|
||||||
template_name = "domain_request_other_contacts.html"
|
template_name = "domain_request_other_contacts.html"
|
||||||
forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
|
forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
|
||||||
|
@ -774,7 +774,10 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
||||||
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
|
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
|
||||||
domain_request.withdraw()
|
domain_request.withdraw()
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
return HttpResponseRedirect(reverse("home"))
|
if self.request.user.is_org_user(self.request):
|
||||||
|
return HttpResponseRedirect(reverse("domain-requests"))
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse("home"))
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
|
@ -793,6 +796,12 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
if status not in valid_statuses:
|
if status not in valid_statuses:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Portfolio users cannot delete their requests if they aren't permissioned to do so
|
||||||
|
if self.request.user.is_org_user(self.request):
|
||||||
|
portfolio = self.request.session.get("portfolio")
|
||||||
|
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -813,7 +822,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
|
|
||||||
# After a delete occurs, do a second sweep on any returned duplicates.
|
# After a delete occurs, do a second sweep on any returned duplicates.
|
||||||
# This determines if any of these three fields share a contact, which is used for
|
# This determines if any of these three fields share a contact, which is used for
|
||||||
# the edge case where the same user may be an SO, and a submitter, for example.
|
# the edge case where the same user may be an SO, and a creator, for example.
|
||||||
if len(duplicates) > 0:
|
if len(duplicates) > 0:
|
||||||
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
||||||
Contact.objects.filter(id__in=duplicates_to_delete).delete()
|
Contact.objects.filter(id__in=duplicates_to_delete).delete()
|
||||||
|
@ -826,7 +835,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
Collects all orphaned contacts associated with a given DomainRequest object.
|
Collects all orphaned contacts associated with a given DomainRequest object.
|
||||||
|
|
||||||
An orphaned contact is defined as a contact that is associated with the domain request,
|
An orphaned contact is defined as a contact that is associated with the domain request,
|
||||||
but not with any other domain_request. This includes the senior official, the submitter,
|
but not with any other domain_request. This includes the senior official, the creator,
|
||||||
and any other contacts linked to the domain_request.
|
and any other contacts linked to the domain_request.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
@ -842,18 +851,16 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
|
|
||||||
# Get each contact object on the DomainRequest object
|
# Get each contact object on the DomainRequest object
|
||||||
so = domain_request.senior_official
|
so = domain_request.senior_official
|
||||||
submitter = domain_request.submitter
|
|
||||||
other_contacts = list(domain_request.other_contacts.all())
|
other_contacts = list(domain_request.other_contacts.all())
|
||||||
other_contact_ids = domain_request.other_contacts.all().values_list("id", flat=True)
|
other_contact_ids = domain_request.other_contacts.all().values_list("id", flat=True)
|
||||||
|
|
||||||
# Check if the desired item still exists in the DB
|
# Check if the desired item still exists in the DB
|
||||||
if check_db:
|
if check_db:
|
||||||
so = self._get_contacts_by_id([so.id]).first() if so is not None else None
|
so = self._get_contacts_by_id([so.id]).first() if so is not None else None
|
||||||
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
|
|
||||||
other_contacts = self._get_contacts_by_id(other_contact_ids)
|
other_contacts = self._get_contacts_by_id(other_contact_ids)
|
||||||
|
|
||||||
# Pair each contact with its db related name for use in checking if it has joins
|
# Pair each contact with its db related name for use in checking if it has joins
|
||||||
checked_contacts = [(so, "senior_official"), (submitter, "submitted_domain_requests")]
|
checked_contacts = [(so, "senior_official")]
|
||||||
checked_contacts.extend((contact, "contact_domain_requests") for contact in other_contacts)
|
checked_contacts.extend((contact, "contact_domain_requests") for contact in other_contacts)
|
||||||
|
|
||||||
for contact, related_name in checked_contacts:
|
for contact, related_name in checked_contacts:
|
||||||
|
|
|
@ -10,17 +10,61 @@ from django.db.models import Q
|
||||||
@login_required
|
@login_required
|
||||||
def get_domain_requests_json(request):
|
def get_domain_requests_json(request):
|
||||||
"""Given the current request,
|
"""Given the current request,
|
||||||
get all domain requests that are associated with the request user and exclude the APPROVED ones"""
|
get all domain requests that are associated with the request user and exclude the APPROVED ones.
|
||||||
|
If we are on the portfolio requests page, limit the response to only those requests associated with
|
||||||
|
the given portfolio."""
|
||||||
|
|
||||||
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
domain_request_ids = get_domain_request_ids_from_request(request)
|
||||||
|
|
||||||
|
objects = DomainRequest.objects.filter(id__in=domain_request_ids)
|
||||||
|
unfiltered_total = objects.count()
|
||||||
|
|
||||||
|
objects = apply_search(objects, request)
|
||||||
|
objects = apply_status_filter(objects, request)
|
||||||
|
objects = apply_sorting(objects, request)
|
||||||
|
|
||||||
|
paginator = Paginator(objects, 10)
|
||||||
|
page_number = request.GET.get("page", 1)
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
domain_requests = [
|
||||||
|
serialize_domain_request(request, domain_request, request.user) for domain_request in page_obj.object_list
|
||||||
|
]
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"domain_requests": domain_requests,
|
||||||
|
"has_next": page_obj.has_next(),
|
||||||
|
"has_previous": page_obj.has_previous(),
|
||||||
|
"page": page_obj.number,
|
||||||
|
"num_pages": paginator.num_pages,
|
||||||
|
"total": paginator.count,
|
||||||
|
"unfiltered_total": unfiltered_total,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_domain_request_ids_from_request(request):
|
||||||
|
"""Get domain request ids from request.
|
||||||
|
|
||||||
|
If portfolio specified, return domain request ids associated with portfolio.
|
||||||
|
Otherwise, return domain request ids associated with request.user.
|
||||||
|
"""
|
||||||
|
portfolio = request.GET.get("portfolio")
|
||||||
|
filter_condition = Q(creator=request.user)
|
||||||
|
if portfolio:
|
||||||
|
if request.user.is_org_user(request) and request.user.has_view_all_requests_portfolio_permission(portfolio):
|
||||||
|
filter_condition = Q(portfolio=portfolio)
|
||||||
|
else:
|
||||||
|
filter_condition = Q(portfolio=portfolio, creator=request.user)
|
||||||
|
domain_requests = DomainRequest.objects.filter(filter_condition).exclude(
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED
|
status=DomainRequest.DomainRequestStatus.APPROVED
|
||||||
)
|
)
|
||||||
unfiltered_total = domain_requests.count()
|
return domain_requests.values_list("id", flat=True)
|
||||||
|
|
||||||
# Handle sorting
|
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
def apply_search(queryset, request):
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
|
||||||
search_term = request.GET.get("search_term")
|
search_term = request.GET.get("search_term")
|
||||||
|
is_portfolio = request.GET.get("portfolio")
|
||||||
|
|
||||||
if search_term:
|
if search_term:
|
||||||
search_term_lower = search_term.lower()
|
search_term_lower = search_term.lower()
|
||||||
|
@ -30,70 +74,92 @@ def get_domain_requests_json(request):
|
||||||
# If yes, we should return domain requests that do not have a
|
# If yes, we should return domain requests that do not have a
|
||||||
# requested_domain (those display as New domain request in the UI)
|
# requested_domain (those display as New domain request in the UI)
|
||||||
if search_term_lower in new_domain_request_text:
|
if search_term_lower in new_domain_request_text:
|
||||||
domain_requests = domain_requests.filter(
|
queryset = queryset.filter(
|
||||||
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
||||||
)
|
)
|
||||||
|
elif is_portfolio:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(requested_domain__name__icontains=search_term)
|
||||||
|
| Q(creator__first_name__icontains=search_term)
|
||||||
|
| Q(creator__last_name__icontains=search_term)
|
||||||
|
| Q(creator__email__icontains=search_term)
|
||||||
|
)
|
||||||
|
# For non org users
|
||||||
else:
|
else:
|
||||||
domain_requests = domain_requests.filter(Q(requested_domain__name__icontains=search_term))
|
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_status_filter(queryset, request):
|
||||||
|
status_param = request.GET.get("status")
|
||||||
|
if status_param:
|
||||||
|
status_list = status_param.split(",")
|
||||||
|
statuses = [status for status in status_list if status in DomainRequest.DomainRequestStatus.values]
|
||||||
|
# Construct Q objects for statuses that can be queried through ORM
|
||||||
|
status_query = Q()
|
||||||
|
if statuses:
|
||||||
|
status_query |= Q(status__in=statuses)
|
||||||
|
# Apply the combined query
|
||||||
|
queryset = queryset.filter(status_query)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_sorting(queryset, request):
|
||||||
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
|
||||||
if order == "desc":
|
if order == "desc":
|
||||||
sort_by = f"-{sort_by}"
|
sort_by = f"-{sort_by}"
|
||||||
domain_requests = domain_requests.order_by(sort_by)
|
return queryset.order_by(sort_by)
|
||||||
page_number = request.GET.get("page", 1)
|
|
||||||
paginator = Paginator(domain_requests, 10)
|
|
||||||
page_obj = paginator.get_page(page_number)
|
|
||||||
|
|
||||||
domain_requests_data = [
|
|
||||||
{
|
def serialize_domain_request(request, domain_request, user):
|
||||||
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
|
|
||||||
"last_submitted_date": domain_request.last_submitted_date,
|
deletable_statuses = [
|
||||||
"status": domain_request.get_status_display(),
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
"id": domain_request.id,
|
|
||||||
"is_deletable": domain_request.status
|
|
||||||
in [DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.WITHDRAWN],
|
|
||||||
"action_url": (
|
|
||||||
reverse("edit-domain-request", kwargs={"id": domain_request.id})
|
|
||||||
if domain_request.status
|
|
||||||
in [
|
|
||||||
DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
|
||||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
]
|
|
||||||
else reverse("domain-request-status", kwargs={"pk": domain_request.id})
|
|
||||||
),
|
|
||||||
"action_label": (
|
|
||||||
"Edit"
|
|
||||||
if domain_request.status
|
|
||||||
in [
|
|
||||||
DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
|
||||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
]
|
|
||||||
else "Manage"
|
|
||||||
),
|
|
||||||
"svg_icon": (
|
|
||||||
"edit"
|
|
||||||
if domain_request.status
|
|
||||||
in [
|
|
||||||
DomainRequest.DomainRequestStatus.STARTED,
|
|
||||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
|
||||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
|
||||||
]
|
|
||||||
else "settings"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for domain_request in page_obj
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return JsonResponse(
|
# Determine if the request is deletable
|
||||||
{
|
if not user.is_org_user(request):
|
||||||
"domain_requests": domain_requests_data,
|
is_deletable = domain_request.status in deletable_statuses
|
||||||
"has_next": page_obj.has_next(),
|
else:
|
||||||
"has_previous": page_obj.has_previous(),
|
portfolio = request.session.get("portfolio")
|
||||||
"page": page_obj.number,
|
is_deletable = (
|
||||||
"num_pages": paginator.num_pages,
|
domain_request.status in deletable_statuses and user.has_edit_request_portfolio_permission(portfolio)
|
||||||
"total": paginator.count,
|
) and domain_request.creator == user
|
||||||
"unfiltered_total": unfiltered_total,
|
|
||||||
}
|
# Determine action label based on user permissions and request status
|
||||||
)
|
editable_statuses = [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
]
|
||||||
|
|
||||||
|
if user.has_edit_request_portfolio_permission and domain_request.creator == user:
|
||||||
|
action_label = "Edit" if domain_request.status in editable_statuses else "Manage"
|
||||||
|
else:
|
||||||
|
action_label = "View"
|
||||||
|
|
||||||
|
# Map the action label to corresponding URLs and icons
|
||||||
|
action_url_map = {
|
||||||
|
"Edit": reverse("edit-domain-request", kwargs={"id": domain_request.id}),
|
||||||
|
"Manage": reverse("domain-request-status", kwargs={"pk": domain_request.id}),
|
||||||
|
"View": "#",
|
||||||
|
}
|
||||||
|
|
||||||
|
svg_icon_map = {"Edit": "edit", "Manage": "settings", "View": "visibility"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
|
||||||
|
"last_submitted_date": domain_request.last_submitted_date,
|
||||||
|
"status": domain_request.get_status_display(),
|
||||||
|
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
|
||||||
|
"creator": domain_request.creator.email,
|
||||||
|
"id": domain_request.id,
|
||||||
|
"is_deletable": is_deletable,
|
||||||
|
"action_url": action_url_map.get(action_label),
|
||||||
|
"action_label": action_label,
|
||||||
|
"svg_icon": svg_icon_map.get(action_label),
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from registrar.models import UserDomainRole, Domain, DomainInformation
|
from registrar.models import UserDomainRole, Domain, DomainInformation, User
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -50,11 +50,16 @@ def get_domain_ids_from_request(request):
|
||||||
"""
|
"""
|
||||||
portfolio = request.GET.get("portfolio")
|
portfolio = request.GET.get("portfolio")
|
||||||
if portfolio:
|
if portfolio:
|
||||||
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
current_user: User = request.user
|
||||||
return domain_infos.values_list("domain_id", flat=True)
|
if current_user.is_org_user(request) and current_user.has_view_all_domains_portfolio_permission(portfolio):
|
||||||
else:
|
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
||||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
return domain_infos.values_list("domain_id", flat=True)
|
||||||
return user_domain_roles.values_list("domain_id", flat=True)
|
else:
|
||||||
|
domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
||||||
|
user_domain_roles = UserDomainRole.objects.filter(user=request.user).values_list("domain_id", flat=True)
|
||||||
|
return domain_info_ids.intersection(user_domain_roles)
|
||||||
|
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||||
|
return user_domain_roles.values_list("domain_id", flat=True)
|
||||||
|
|
||||||
|
|
||||||
def apply_search(queryset, request):
|
def apply_search(queryset, request):
|
||||||
|
|
|
@ -42,12 +42,41 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
||||||
|
|
||||||
|
|
||||||
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
||||||
"""Some users have access to the underlying portfolio, but not any domains.
|
"""Some users have access to the underlying portfolio, but not any domains.
|
||||||
This is a custom view which explains that to the user - and denotes who to contact.
|
This is a custom view which explains that to the user - and denotes who to contact.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = Portfolio
|
model = Portfolio
|
||||||
template_name = "no_portfolio_domains.html"
|
template_name = "portfolio_no_domains.html"
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return render(request, self.template_name, context=self.get_context_data())
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Add additional context data to the template."""
|
||||||
|
# We can override the base class. This view only needs this item.
|
||||||
|
context = {}
|
||||||
|
portfolio = self.request.session.get("portfolio")
|
||||||
|
if portfolio:
|
||||||
|
admin_ids = UserPortfolioPermission.objects.filter(
|
||||||
|
portfolio=portfolio,
|
||||||
|
roles__overlap=[
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
],
|
||||||
|
).values_list("user__id", flat=True)
|
||||||
|
|
||||||
|
admin_users = User.objects.filter(id__in=admin_ids)
|
||||||
|
context["portfolio_administrators"] = admin_users
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, View):
|
||||||
|
"""Some users have access to the underlying portfolio, but not any domain requests.
|
||||||
|
This is a custom view which explains that to the user - and denotes who to contact.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Portfolio
|
||||||
|
template_name = "portfolio_no_requests.html"
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return render(request, self.template_name, context=self.get_context_data())
|
return render(request, self.template_name, context=self.get_context_data())
|
||||||
|
|
|
@ -11,7 +11,6 @@ from django.urls import NoReverseMatch, reverse
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
from registrar.views.utility.permission_views import UserProfilePermissionView
|
from registrar.views.utility.permission_views import UserProfilePermissionView
|
||||||
from waffle.decorators import waffle_flag
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -46,7 +45,6 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
|
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
@waffle_flag("profile_feature") # type: ignore
|
|
||||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -81,12 +81,12 @@ class OrderableFieldsMixin:
|
||||||
Or for fields with multiple order_fields:
|
Or for fields with multiple order_fields:
|
||||||
|
|
||||||
```
|
```
|
||||||
def get_sortable_submitter(self, obj):
|
def get_sortable_creator(self, obj):
|
||||||
return obj.submitter
|
return obj.creator
|
||||||
# Allows column order sorting
|
# Allows column order sorting
|
||||||
get_sortable_submitter.admin_order_field = ["submitter__first_name", "submitter__last_name"]
|
get_sortable_creator.admin_order_field = ["creator__first_name", "creator__last_name"]
|
||||||
# Sets column's header
|
# Sets column's header
|
||||||
get_sortable_submitter.short_description = "submitter"
|
get_sortable_creator.short_description = "creator"
|
||||||
```
|
```
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
@ -114,8 +114,8 @@ class OrderableFieldsMixin:
|
||||||
|
|
||||||
Returns (example):
|
Returns (example):
|
||||||
```
|
```
|
||||||
def get_submitter(self, obj):
|
def get_creator(self, obj):
|
||||||
return obj.submitter
|
return obj.creator
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
attr = getattr(obj, field)
|
attr = getattr(obj, field)
|
||||||
|
@ -433,7 +433,7 @@ class PortfolioDomainsPermission(PortfolioBasePermission):
|
||||||
up from the portfolio's primary key in self.kwargs["pk"]"""
|
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||||
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
portfolio = self.request.session.get("portfolio")
|
||||||
if not self.request.user.has_domains_portfolio_permission(portfolio):
|
if not self.request.user.has_any_domains_portfolio_permission(portfolio):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return super().has_permission()
|
return super().has_permission()
|
||||||
|
@ -450,7 +450,7 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
|
||||||
up from the portfolio's primary key in self.kwargs["pk"]"""
|
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||||
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
portfolio = self.request.session.get("portfolio")
|
||||||
if not self.request.user.has_domain_requests_portfolio_permission(portfolio):
|
if not self.request.user.has_any_requests_portfolio_permission(portfolio):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return super().has_permission()
|
return super().has_permission()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue