mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-22 04:19:26 +02:00
Merge branch 'main' into dk/2205-import-update-epp
This commit is contained in:
commit
1921d99eab
80 changed files with 11100 additions and 701 deletions
2
.github/workflows/deploy-sandbox.yaml
vendored
2
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -25,6 +25,8 @@ jobs:
|
||||||
|| startsWith(github.head_ref, 'meoward/')
|
|| startsWith(github.head_ref, 'meoward/')
|
||||||
|| startsWith(github.head_ref, 'bob/')
|
|| startsWith(github.head_ref, 'bob/')
|
||||||
|| startsWith(github.head_ref, 'cb/')
|
|| startsWith(github.head_ref, 'cb/')
|
||||||
|
|| startsWith(github.head_ref, 'hotgov/')
|
||||||
|
|| startsWith(github.head_ref, 'litterbox/')
|
||||||
outputs:
|
outputs:
|
||||||
environment: ${{ steps.var.outputs.environment}}
|
environment: ${{ steps.var.outputs.environment}}
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
|
2
.github/workflows/migrate.yaml
vendored
2
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,8 @@ on:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- litterbox
|
||||||
|
- hotgov
|
||||||
- cb
|
- cb
|
||||||
- bob
|
- bob
|
||||||
- meoward
|
- meoward
|
||||||
|
|
2
.github/workflows/reset-db.yaml
vendored
2
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,8 @@ on:
|
||||||
options:
|
options:
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- litterbox
|
||||||
|
- hotgov
|
||||||
- cb
|
- cb
|
||||||
- bob
|
- bob
|
||||||
- meoward
|
- meoward
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
# Complete model documentation
|
# Complete model documentation
|
||||||
|
|
||||||
This is an auto-generated diagram of our data models generated with the
|
This is an auto-generated diagram of our data models generated with the
|
||||||
[django-model2puml](https://github.com/sen-den/django-model2puml) library
|
[django-model2puml](https://github.com/sen-den/django-model2puml) library.
|
||||||
using the command
|
|
||||||
|
## How to generate the puml
|
||||||
|
|
||||||
|
1. Uncomment `puml_generator` from `INSTALLED_APPS` in settings.py and docker-compose down and up
|
||||||
|
2. Run the following command to generate a puml file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker compose app ./manage.py generate_puml --include registrar
|
docker compose exec app ./manage.py generate_puml --include registrar
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
@ -13,12 +17,19 @@ $ docker compose app ./manage.py generate_puml --include registrar
|
||||||
<details>
|
<details>
|
||||||
<summary>PlantUML source code</summary>
|
<summary>PlantUML source code</summary>
|
||||||
|
|
||||||
To regenerate this image using Docker, run
|
## How To regenerate the database svg image
|
||||||
|
|
||||||
|
1. Copy your puml file contents into the bottom of this file and replace the current code marked by `plantuml`
|
||||||
|
2. Run the following command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md
|
docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. Remove the puml file from earlier (if you still have it)
|
||||||
|
4. Commit the new image and the md file
|
||||||
|
|
||||||
|
|
||||||
```plantuml
|
```plantuml
|
||||||
@startuml
|
@startuml
|
||||||
class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
|
class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
|
||||||
|
@ -28,17 +39,97 @@ class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
|
||||||
+ created_at (DateTimeField)
|
+ created_at (DateTimeField)
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
~ user (OneToOneField)
|
~ user (OneToOneField)
|
||||||
+ first_name (TextField)
|
+ first_name (CharField)
|
||||||
+ middle_name (TextField)
|
+ middle_name (CharField)
|
||||||
+ last_name (TextField)
|
+ last_name (CharField)
|
||||||
+ title (TextField)
|
+ title (CharField)
|
||||||
+ email (TextField)
|
+ email (EmailField)
|
||||||
+ phone (PhoneNumberField)
|
+ phone (PhoneNumberField)
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
registrar.Contact -- registrar.User
|
registrar.Contact -- registrar.User
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.Host <Registrar>" as registrar.Host #d6f4e9 {
|
||||||
|
host
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
+ name (CharField)
|
||||||
|
~ domain (ForeignKey)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
registrar.Host -- registrar.Domain
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.HostIP <Registrar>" as registrar.HostIP #d6f4e9 {
|
||||||
|
host ip
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
+ address (CharField)
|
||||||
|
~ host (ForeignKey)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
registrar.HostIP -- registrar.Host
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.PublicContact <Registrar>" as registrar.PublicContact #d6f4e9 {
|
||||||
|
public contact
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
+ contact_type (CharField)
|
||||||
|
+ registry_id (CharField)
|
||||||
|
~ domain (ForeignKey)
|
||||||
|
+ name (CharField)
|
||||||
|
+ org (CharField)
|
||||||
|
+ street1 (CharField)
|
||||||
|
+ street2 (CharField)
|
||||||
|
+ street3 (CharField)
|
||||||
|
+ city (CharField)
|
||||||
|
+ sp (CharField)
|
||||||
|
+ pc (CharField)
|
||||||
|
+ cc (CharField)
|
||||||
|
+ email (EmailField)
|
||||||
|
+ voice (CharField)
|
||||||
|
+ fax (CharField)
|
||||||
|
+ pw (CharField)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
registrar.PublicContact -- registrar.Domain
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.Domain <Registrar>" as registrar.Domain #d6f4e9 {
|
||||||
|
domain
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
+ name (DomainField)
|
||||||
|
+ state (FSMField)
|
||||||
|
+ expiration_date (DateField)
|
||||||
|
+ security_contact_registry_id (TextField)
|
||||||
|
+ deleted (DateField)
|
||||||
|
+ first_ready (DateField)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.FederalAgency <Registrar>" as registrar.FederalAgency #d6f4e9 {
|
||||||
|
Federal agency
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ created_at (DateTimeField)
|
||||||
|
+ updated_at (DateTimeField)
|
||||||
|
+ agency (CharField)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
||||||
domain request
|
domain request
|
||||||
--
|
--
|
||||||
|
@ -46,24 +137,25 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
||||||
+ created_at (DateTimeField)
|
+ created_at (DateTimeField)
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
+ status (FSMField)
|
+ status (FSMField)
|
||||||
|
+ rejection_reason (TextField)
|
||||||
|
~ federal_agency (ForeignKey)
|
||||||
~ creator (ForeignKey)
|
~ creator (ForeignKey)
|
||||||
~ investigator (ForeignKey)
|
~ investigator (ForeignKey)
|
||||||
|
+ generic_org_type (CharField)
|
||||||
|
+ is_election_board (BooleanField)
|
||||||
+ organization_type (CharField)
|
+ organization_type (CharField)
|
||||||
+ federally_recognized_tribe (BooleanField)
|
+ federally_recognized_tribe (BooleanField)
|
||||||
+ state_recognized_tribe (BooleanField)
|
+ state_recognized_tribe (BooleanField)
|
||||||
+ tribe_name (TextField)
|
+ tribe_name (CharField)
|
||||||
+ federal_agency (TextField)
|
|
||||||
+ federal_type (CharField)
|
+ federal_type (CharField)
|
||||||
+ is_election_board (BooleanField)
|
+ organization_name (CharField)
|
||||||
+ organization_name (TextField)
|
+ address_line1 (CharField)
|
||||||
+ address_line1 (TextField)
|
|
||||||
+ address_line2 (CharField)
|
+ address_line2 (CharField)
|
||||||
+ city (TextField)
|
+ city (CharField)
|
||||||
+ state_territory (CharField)
|
+ state_territory (CharField)
|
||||||
+ zipcode (CharField)
|
+ zipcode (CharField)
|
||||||
+ urbanization (TextField)
|
+ urbanization (CharField)
|
||||||
+ type_of_work (TextField)
|
+ about_your_organization (TextField)
|
||||||
+ more_organization_information (TextField)
|
|
||||||
~ authorizing_official (ForeignKey)
|
~ authorizing_official (ForeignKey)
|
||||||
~ approved_domain (OneToOneField)
|
~ approved_domain (OneToOneField)
|
||||||
~ requested_domain (OneToOneField)
|
~ requested_domain (OneToOneField)
|
||||||
|
@ -71,17 +163,23 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
|
||||||
+ purpose (TextField)
|
+ purpose (TextField)
|
||||||
+ no_other_contacts_rationale (TextField)
|
+ no_other_contacts_rationale (TextField)
|
||||||
+ anything_else (TextField)
|
+ anything_else (TextField)
|
||||||
|
+ has_anything_else_text (BooleanField)
|
||||||
|
+ cisa_representative_email (EmailField)
|
||||||
|
+ has_cisa_representative (BooleanField)
|
||||||
+ is_policy_acknowledged (BooleanField)
|
+ is_policy_acknowledged (BooleanField)
|
||||||
|
+ submission_date (DateField)
|
||||||
|
+ notes (TextField)
|
||||||
# current_websites (ManyToManyField)
|
# current_websites (ManyToManyField)
|
||||||
# alternative_domains (ManyToManyField)
|
# alternative_domains (ManyToManyField)
|
||||||
# other_contacts (ManyToManyField)
|
# other_contacts (ManyToManyField)
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
|
registrar.DomainRequest -- registrar.FederalAgency
|
||||||
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.DraftDomain
|
|
||||||
registrar.DomainRequest -- registrar.Domain
|
registrar.DomainRequest -- registrar.Domain
|
||||||
|
registrar.DomainRequest -- registrar.DraftDomain
|
||||||
registrar.DomainRequest -- registrar.Contact
|
registrar.DomainRequest -- registrar.Contact
|
||||||
registrar.DomainRequest *--* registrar.Website
|
registrar.DomainRequest *--* registrar.Website
|
||||||
registrar.DomainRequest *--* registrar.Website
|
registrar.DomainRequest *--* registrar.Website
|
||||||
|
@ -94,35 +192,37 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
|
||||||
+ id (BigAutoField)
|
+ id (BigAutoField)
|
||||||
+ created_at (DateTimeField)
|
+ created_at (DateTimeField)
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
|
~ federal_agency (ForeignKey)
|
||||||
~ creator (ForeignKey)
|
~ creator (ForeignKey)
|
||||||
~ domain_request (OneToOneField)
|
~ domain_request (OneToOneField)
|
||||||
|
+ generic_org_type (CharField)
|
||||||
+ organization_type (CharField)
|
+ organization_type (CharField)
|
||||||
+ federally_recognized_tribe (BooleanField)
|
+ federally_recognized_tribe (BooleanField)
|
||||||
+ state_recognized_tribe (BooleanField)
|
+ state_recognized_tribe (BooleanField)
|
||||||
+ tribe_name (TextField)
|
+ tribe_name (CharField)
|
||||||
+ federal_agency (TextField)
|
|
||||||
+ federal_type (CharField)
|
+ federal_type (CharField)
|
||||||
+ is_election_board (BooleanField)
|
+ is_election_board (BooleanField)
|
||||||
+ organization_name (TextField)
|
+ organization_name (CharField)
|
||||||
+ address_line1 (TextField)
|
+ address_line1 (CharField)
|
||||||
+ address_line2 (CharField)
|
+ address_line2 (CharField)
|
||||||
+ city (TextField)
|
+ city (CharField)
|
||||||
+ state_territory (CharField)
|
+ state_territory (CharField)
|
||||||
+ zipcode (CharField)
|
+ zipcode (CharField)
|
||||||
+ urbanization (TextField)
|
+ urbanization (CharField)
|
||||||
+ type_of_work (TextField)
|
+ about_your_organization (TextField)
|
||||||
+ more_organization_information (TextField)
|
|
||||||
~ authorizing_official (ForeignKey)
|
~ authorizing_official (ForeignKey)
|
||||||
~ domain (OneToOneField)
|
~ domain (OneToOneField)
|
||||||
~ submitter (ForeignKey)
|
~ submitter (ForeignKey)
|
||||||
+ purpose (TextField)
|
+ purpose (TextField)
|
||||||
+ no_other_contacts_rationale (TextField)
|
+ no_other_contacts_rationale (TextField)
|
||||||
+ anything_else (TextField)
|
+ anything_else (TextField)
|
||||||
|
+ cisa_representative_email (EmailField)
|
||||||
+ is_policy_acknowledged (BooleanField)
|
+ is_policy_acknowledged (BooleanField)
|
||||||
+ security_email (EmailField)
|
+ notes (TextField)
|
||||||
# other_contacts (ManyToManyField)
|
# other_contacts (ManyToManyField)
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
|
registrar.DomainInformation -- registrar.FederalAgency
|
||||||
registrar.DomainInformation -- registrar.User
|
registrar.DomainInformation -- registrar.User
|
||||||
registrar.DomainInformation -- registrar.DomainRequest
|
registrar.DomainInformation -- registrar.DomainRequest
|
||||||
registrar.DomainInformation -- registrar.Contact
|
registrar.DomainInformation -- registrar.Contact
|
||||||
|
@ -142,43 +242,6 @@ class "registrar.DraftDomain <Registrar>" as registrar.DraftDomain #d6f4e9 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class "registrar.Domain <Registrar>" as registrar.Domain #d6f4e9 {
|
|
||||||
domain
|
|
||||||
--
|
|
||||||
+ id (BigAutoField)
|
|
||||||
+ created_at (DateTimeField)
|
|
||||||
+ updated_at (DateTimeField)
|
|
||||||
+ name (CharField)
|
|
||||||
--
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class "registrar.HostIP <Registrar>" as registrar.HostIP #d6f4e9 {
|
|
||||||
host ip
|
|
||||||
--
|
|
||||||
+ id (BigAutoField)
|
|
||||||
+ created_at (DateTimeField)
|
|
||||||
+ updated_at (DateTimeField)
|
|
||||||
+ address (CharField)
|
|
||||||
~ host (ForeignKey)
|
|
||||||
--
|
|
||||||
}
|
|
||||||
registrar.HostIP -- registrar.Host
|
|
||||||
|
|
||||||
|
|
||||||
class "registrar.Host <Registrar>" as registrar.Host #d6f4e9 {
|
|
||||||
host
|
|
||||||
--
|
|
||||||
+ id (BigAutoField)
|
|
||||||
+ created_at (DateTimeField)
|
|
||||||
+ updated_at (DateTimeField)
|
|
||||||
+ name (CharField)
|
|
||||||
~ domain (ForeignKey)
|
|
||||||
--
|
|
||||||
}
|
|
||||||
registrar.Host -- registrar.Domain
|
|
||||||
|
|
||||||
|
|
||||||
class "registrar.UserDomainRole <Registrar>" as registrar.UserDomainRole #d6f4e9 {
|
class "registrar.UserDomainRole <Registrar>" as registrar.UserDomainRole #d6f4e9 {
|
||||||
user domain role
|
user domain role
|
||||||
--
|
--
|
||||||
|
@ -208,47 +271,49 @@ class "registrar.DomainInvitation <Registrar>" as registrar.DomainInvitation #d6
|
||||||
registrar.DomainInvitation -- registrar.Domain
|
registrar.DomainInvitation -- registrar.Domain
|
||||||
|
|
||||||
|
|
||||||
class "registrar.Nameserver <Registrar>" as registrar.Nameserver #d6f4e9 {
|
class "registrar.TransitionDomain <Registrar>" as registrar.TransitionDomain #d6f4e9 {
|
||||||
nameserver
|
transition domain
|
||||||
--
|
--
|
||||||
+ id (BigAutoField)
|
+ id (BigAutoField)
|
||||||
+ created_at (DateTimeField)
|
+ created_at (DateTimeField)
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
+ name (CharField)
|
+ username (CharField)
|
||||||
~ domain (ForeignKey)
|
+ domain_name (CharField)
|
||||||
~ host_ptr (OneToOneField)
|
+ status (CharField)
|
||||||
|
+ email_sent (BooleanField)
|
||||||
|
+ processed (BooleanField)
|
||||||
|
+ generic_org_type (CharField)
|
||||||
|
+ organization_name (CharField)
|
||||||
|
+ federal_type (CharField)
|
||||||
|
+ federal_agency (CharField)
|
||||||
|
+ epp_creation_date (DateField)
|
||||||
|
+ epp_expiration_date (DateField)
|
||||||
|
+ first_name (CharField)
|
||||||
|
+ middle_name (CharField)
|
||||||
|
+ last_name (CharField)
|
||||||
|
+ title (CharField)
|
||||||
|
+ email (EmailField)
|
||||||
|
+ phone (CharField)
|
||||||
|
+ address_line (CharField)
|
||||||
|
+ city (CharField)
|
||||||
|
+ state_territory (CharField)
|
||||||
|
+ zipcode (CharField)
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
registrar.Nameserver -- registrar.Domain
|
|
||||||
registrar.Nameserver -- registrar.Host
|
|
||||||
|
|
||||||
|
|
||||||
class "registrar.PublicContact <Registrar>" as registrar.PublicContact #d6f4e9 {
|
class "registrar.VerifiedByStaff <Registrar>" as registrar.VerifiedByStaff #d6f4e9 {
|
||||||
public contact
|
verified by staff
|
||||||
--
|
--
|
||||||
+ id (BigAutoField)
|
+ id (BigAutoField)
|
||||||
+ created_at (DateTimeField)
|
+ created_at (DateTimeField)
|
||||||
+ updated_at (DateTimeField)
|
+ updated_at (DateTimeField)
|
||||||
+ contact_type (CharField)
|
+ email (EmailField)
|
||||||
+ registry_id (CharField)
|
~ requestor (ForeignKey)
|
||||||
~ domain (ForeignKey)
|
+ notes (TextField)
|
||||||
+ name (TextField)
|
|
||||||
+ org (TextField)
|
|
||||||
+ street1 (TextField)
|
|
||||||
+ street2 (TextField)
|
|
||||||
+ street3 (TextField)
|
|
||||||
+ city (TextField)
|
|
||||||
+ sp (TextField)
|
|
||||||
+ pc (TextField)
|
|
||||||
+ cc (TextField)
|
|
||||||
+ email (TextField)
|
|
||||||
+ voice (TextField)
|
|
||||||
+ fax (TextField)
|
|
||||||
+ pw (TextField)
|
|
||||||
--
|
--
|
||||||
}
|
}
|
||||||
|
registrar.VerifiedByStaff -- registrar.User
|
||||||
registrar.PublicContact -- registrar.Domain
|
|
||||||
|
|
||||||
|
|
||||||
class "registrar.User <Registrar>" as registrar.User #d6f4e9 {
|
class "registrar.User <Registrar>" as registrar.User #d6f4e9 {
|
||||||
|
@ -265,7 +330,11 @@ class "registrar.User <Registrar>" as registrar.User #d6f4e9 {
|
||||||
+ is_staff (BooleanField)
|
+ is_staff (BooleanField)
|
||||||
+ is_active (BooleanField)
|
+ is_active (BooleanField)
|
||||||
+ date_joined (DateTimeField)
|
+ date_joined (DateTimeField)
|
||||||
|
+ status (CharField)
|
||||||
+ phone (PhoneNumberField)
|
+ phone (PhoneNumberField)
|
||||||
|
+ middle_name (CharField)
|
||||||
|
+ title (CharField)
|
||||||
|
+ verification_type (CharField)
|
||||||
# groups (ManyToManyField)
|
# groups (ManyToManyField)
|
||||||
# user_permissions (ManyToManyField)
|
# user_permissions (ManyToManyField)
|
||||||
# domains (ManyToManyField)
|
# domains (ManyToManyField)
|
||||||
|
@ -274,6 +343,17 @@ class "registrar.User <Registrar>" as registrar.User #d6f4e9 {
|
||||||
registrar.User *--* registrar.Domain
|
registrar.User *--* registrar.Domain
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.UserGroup <Registrar>" as registrar.UserGroup #d6f4e9 {
|
||||||
|
User group
|
||||||
|
--
|
||||||
|
- id (AutoField)
|
||||||
|
+ name (CharField)
|
||||||
|
~ group_ptr (OneToOneField)
|
||||||
|
# permissions (ManyToManyField)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class "registrar.Website <Registrar>" as registrar.Website #d6f4e9 {
|
class "registrar.Website <Registrar>" as registrar.Website #d6f4e9 {
|
||||||
website
|
website
|
||||||
--
|
--
|
||||||
|
@ -285,6 +365,29 @@ class "registrar.Website <Registrar>" as registrar.Website #d6f4e9 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class "registrar.WaffleFlag <Registrar>" as registrar.WaffleFlag #d6f4e9 {
|
||||||
|
waffle flag
|
||||||
|
--
|
||||||
|
+ id (BigAutoField)
|
||||||
|
+ name (CharField)
|
||||||
|
+ everyone (BooleanField)
|
||||||
|
+ percent (DecimalField)
|
||||||
|
+ testing (BooleanField)
|
||||||
|
+ superusers (BooleanField)
|
||||||
|
+ staff (BooleanField)
|
||||||
|
+ authenticated (BooleanField)
|
||||||
|
+ languages (TextField)
|
||||||
|
+ rollout (BooleanField)
|
||||||
|
+ note (TextField)
|
||||||
|
+ created (DateTimeField)
|
||||||
|
+ modified (DateTimeField)
|
||||||
|
# groups (ManyToManyField)
|
||||||
|
# users (ManyToManyField)
|
||||||
|
--
|
||||||
|
}
|
||||||
|
registrar.WaffleFlag *--* registrar.User
|
||||||
|
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 97 KiB |
|
@ -291,13 +291,13 @@ We use the [CSS Block Element Modifier (BEM)](https://getbem.com/naming/) naming
|
||||||
|
|
||||||
### Upgrading USWDS and other JavaScript packages
|
### Upgrading USWDS and other JavaScript packages
|
||||||
|
|
||||||
Version numbers can be manually controlled in `package.json`. Edit that, if desired.
|
1. Version numbers can be manually controlled in `package.json`. Edit that, if desired.
|
||||||
|
2. Now run `docker-compose run node npm update`.
|
||||||
Now run `docker-compose run node npm update`.
|
3. Then run `docker-compose up` to recompile and recopy the assets, or run `docker-compose updateUswds` if your docker is already up.
|
||||||
|
4. Make note of the dotgov changes in uswds-edited.js.
|
||||||
Then run `docker-compose up` to recompile and recopy the assets.
|
5. Copy over the newly compiled code from uswds.js into uswds-edited.js.
|
||||||
|
6. Put back the dotgov changes you made note of into uswds-edited.js.
|
||||||
Examine the results in the running application (remember to empty your cache!) and commit `package.json` and `package-lock.json` if all is well.
|
7. Examine the results in the running application (remember to empty your cache!) and commit `package.json` and `package-lock.json` if all is well.
|
||||||
|
|
||||||
## Finite State Machines
|
## Finite State Machines
|
||||||
|
|
||||||
|
@ -320,16 +320,6 @@ it may help to resync your laptop with time.nist.gov:
|
||||||
sudo sntp -sS time.nist.gov
|
sudo sntp -sS time.nist.gov
|
||||||
```
|
```
|
||||||
|
|
||||||
### Settings
|
|
||||||
The config for the connection pool exists inside the `settings.py` file.
|
|
||||||
| Name | Purpose |
|
|
||||||
| ------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
||||||
| EPP_CONNECTION_POOL_SIZE | Determines the number of concurrent sockets that should exist in the pool. |
|
|
||||||
| POOL_KEEP_ALIVE | Determines the interval in which we ping open connections in seconds. Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE |
|
|
||||||
| POOL_TIMEOUT | Determines how long we try to keep a pool alive for, before restarting it. |
|
|
||||||
|
|
||||||
Consider updating the `POOL_TIMEOUT` or `POOL_KEEP_ALIVE` periods if the pool often restarts. If the pool only restarts after a period of inactivity, update `POOL_KEEP_ALIVE`. If it restarts during the EPP call itself, then `POOL_TIMEOUT` needs to be updated.
|
|
||||||
|
|
||||||
## Adding a S3 instance to your sandbox
|
## Adding a S3 instance to your sandbox
|
||||||
This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you.
|
This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you.
|
||||||
|
|
||||||
|
@ -405,3 +395,9 @@ This function is triggered by the post_save event on the User model, designed to
|
||||||
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
|
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
|
||||||
|
|
||||||
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
|
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
|
||||||
|
|
||||||
|
## Disable email sending (toggling the disable_email_sending flag)
|
||||||
|
1. On the app, navigate to `\admin`.
|
||||||
|
2. Under models, click `Waffle flags`.
|
||||||
|
3. Click the `disable_email_sending` record. This should exist by default, if not - create one with that name.
|
||||||
|
4. (Important) Set the field `everyone` to `Yes`. This field overrides all other settings
|
|
@ -668,3 +668,32 @@ Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
#### Step 1: Running the script
|
#### Step 1: Running the script
|
||||||
```docker-compose exec app ./manage.py populate_verification_type```
|
```docker-compose exec app ./manage.py populate_verification_type```
|
||||||
|
|
||||||
|
|
||||||
|
## Copy names from contacts to users
|
||||||
|
|
||||||
|
### Running on sandboxes
|
||||||
|
|
||||||
|
#### Step 1: Login to CloudFoundry
|
||||||
|
```cf login -a api.fr.cloud.gov --sso```
|
||||||
|
|
||||||
|
#### Step 2: SSH into your environment
|
||||||
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
|
Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
|
#### Step 3: Create a shell instance
|
||||||
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
|
#### Step 4: Running the script
|
||||||
|
```./manage.py copy_names_from_contacts_to_users --debug```
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
|
||||||
|
#### Step 1: Running the script
|
||||||
|
```docker-compose exec app ./manage.py copy_names_from_contacts_to_users --debug```
|
||||||
|
|
||||||
|
##### Optional parameters
|
||||||
|
| | Parameter | Description |
|
||||||
|
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
|
||||||
|
| 1 | **debug** | Increases logging detail. Defaults to False. |
|
||||||
|
|
32
ops/manifests/manifest-hotgov.yaml
Normal file
32
ops/manifests/manifest-hotgov.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-hotgov
|
||||||
|
buildpacks:
|
||||||
|
- python_buildpack
|
||||||
|
path: ../../src
|
||||||
|
instances: 1
|
||||||
|
memory: 512M
|
||||||
|
stack: cflinuxfs4
|
||||||
|
timeout: 180
|
||||||
|
command: ./run.sh
|
||||||
|
health-check-type: http
|
||||||
|
health-check-http-endpoint: /health
|
||||||
|
health-check-invocation-timeout: 40
|
||||||
|
env:
|
||||||
|
# Send stdout and stderr straight to the terminal without buffering
|
||||||
|
PYTHONUNBUFFERED: yup
|
||||||
|
# Tell Django where to find its configuration
|
||||||
|
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||||
|
# Tell Django where it is being hosted
|
||||||
|
DJANGO_BASE_URL: https://getgov-hotgov.app.cloud.gov
|
||||||
|
# Tell Django how much stuff to log
|
||||||
|
DJANGO_LOG_LEVEL: INFO
|
||||||
|
# default public site location
|
||||||
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
|
# Flag to disable/enable features in prod environments
|
||||||
|
IS_PRODUCTION: False
|
||||||
|
routes:
|
||||||
|
- route: getgov-hotgov.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-hotgov-database
|
32
ops/manifests/manifest-litterbox.yaml
Normal file
32
ops/manifests/manifest-litterbox.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-litterbox
|
||||||
|
buildpacks:
|
||||||
|
- python_buildpack
|
||||||
|
path: ../../src
|
||||||
|
instances: 1
|
||||||
|
memory: 512M
|
||||||
|
stack: cflinuxfs4
|
||||||
|
timeout: 180
|
||||||
|
command: ./run.sh
|
||||||
|
health-check-type: http
|
||||||
|
health-check-http-endpoint: /health
|
||||||
|
health-check-invocation-timeout: 40
|
||||||
|
env:
|
||||||
|
# Send stdout and stderr straight to the terminal without buffering
|
||||||
|
PYTHONUNBUFFERED: yup
|
||||||
|
# Tell Django where to find its configuration
|
||||||
|
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||||
|
# Tell Django where it is being hosted
|
||||||
|
DJANGO_BASE_URL: https://getgov-litterbox.app.cloud.gov
|
||||||
|
# Tell Django how much stuff to log
|
||||||
|
DJANGO_LOG_LEVEL: INFO
|
||||||
|
# default public site location
|
||||||
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
|
# Flag to disable/enable features in prod environments
|
||||||
|
IS_PRODUCTION: False
|
||||||
|
routes:
|
||||||
|
- route: getgov-litterbox.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-litterbox-database
|
|
@ -15,6 +15,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from dateutil.relativedelta import relativedelta # type: ignore
|
from dateutil.relativedelta import relativedelta # type: ignore
|
||||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from waffle.admin import FlagAdmin
|
from waffle.admin import FlagAdmin
|
||||||
from waffle.models import Sample, Switch
|
from waffle.models import Sample, Switch
|
||||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
||||||
|
@ -383,6 +384,39 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
||||||
change_form_template = "admin/change_form_no_submit.html"
|
change_form_template = "admin/change_form_no_submit.html"
|
||||||
add_form_template = "admin/change_form_no_submit.html"
|
add_form_template = "admin/change_form_no_submit.html"
|
||||||
|
|
||||||
|
# Select log entry to change -> Log entries
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Log entries"
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
# #786: Skipping on updating audit log tab titles for now
|
||||||
|
# def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
|
# if extra_context is None:
|
||||||
|
# extra_context = {}
|
||||||
|
|
||||||
|
# log_entry = self.get_object(request, object_id)
|
||||||
|
|
||||||
|
# if log_entry:
|
||||||
|
# # Reset title to empty string
|
||||||
|
# extra_context["subtitle"] = ""
|
||||||
|
# extra_context["tabtitle"] = ""
|
||||||
|
|
||||||
|
# object_repr = log_entry.object_repr # Hold name of the object
|
||||||
|
# changes = log_entry.changes
|
||||||
|
|
||||||
|
# # Check if this is a log entry for an addition and related to the contact model
|
||||||
|
# # Created [name] -> Created [name] contact | Change log entry
|
||||||
|
# if (
|
||||||
|
# all(new_value != "None" for field, (old_value, new_value) in changes.items())
|
||||||
|
# and log_entry.content_type.model == "contact"
|
||||||
|
# ):
|
||||||
|
# extra_context["subtitle"] = f"Created {object_repr} contact"
|
||||||
|
# extra_context["tabtitle"] = "Change log entry"
|
||||||
|
|
||||||
|
# return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class AdminSortFields:
|
class AdminSortFields:
|
||||||
_name_sort = ["first_name", "last_name", "email"]
|
_name_sort = ["first_name", "last_name", "email"]
|
||||||
|
@ -555,6 +589,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
resource_classes = [UserResource]
|
resource_classes = [UserResource]
|
||||||
|
|
||||||
form = MyUserAdminForm
|
form = MyUserAdminForm
|
||||||
|
change_form_template = "django/admin/user_change_form.html"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Contains meta information about this class"""
|
"""Contains meta information about this class"""
|
||||||
|
@ -594,7 +629,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
None,
|
None,
|
||||||
{"fields": ("username", "password", "status", "verification_type")},
|
{"fields": ("username", "password", "status", "verification_type")},
|
||||||
),
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
|
("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||||
(
|
(
|
||||||
"Permissions",
|
"Permissions",
|
||||||
{
|
{
|
||||||
|
@ -625,7 +660,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
|
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||||
(
|
(
|
||||||
"Permissions",
|
"Permissions",
|
||||||
{
|
{
|
||||||
|
@ -673,8 +708,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
ordering = ["first_name", "last_name", "email"]
|
ordering = ["first_name", "last_name", "email"]
|
||||||
search_help_text = "Search by first name, last name, or email."
|
search_help_text = "Search by first name, last name, or email."
|
||||||
|
|
||||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
|
||||||
|
|
||||||
def get_search_results(self, request, queryset, search_term):
|
def get_search_results(self, request, queryset, search_term):
|
||||||
"""
|
"""
|
||||||
Override for get_search_results. This affects any upstream model using autocomplete_fields,
|
Override for get_search_results. This affects any upstream model using autocomplete_fields,
|
||||||
|
@ -754,6 +787,23 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
# users who might not belong to groups
|
# users who might not belong to groups
|
||||||
return self.analyst_readonly_fields
|
return self.analyst_readonly_fields
|
||||||
|
|
||||||
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
|
"""Add user's related domains and requests to context"""
|
||||||
|
obj = self.get_object(request, object_id)
|
||||||
|
|
||||||
|
domain_requests = DomainRequest.objects.filter(creator=obj).exclude(
|
||||||
|
Q(status=DomainRequest.DomainRequestStatus.STARTED) | Q(status=DomainRequest.DomainRequestStatus.WITHDRAWN)
|
||||||
|
)
|
||||||
|
sort_by = request.GET.get("sort_by", "requested_domain__name")
|
||||||
|
domain_requests = domain_requests.order_by(sort_by)
|
||||||
|
|
||||||
|
user_domain_roles = UserDomainRole.objects.filter(user=obj)
|
||||||
|
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||||
|
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
|
||||||
|
|
||||||
|
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
||||||
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
|
||||||
class HostIPInline(admin.StackedInline):
|
class HostIPInline(admin.StackedInline):
|
||||||
"""Edit an ip address on the host page."""
|
"""Edit an ip address on the host page."""
|
||||||
|
@ -778,6 +828,14 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||||
search_help_text = "Search by domain or host name."
|
search_help_text = "Search by domain or host name."
|
||||||
inlines = [HostIPInline]
|
inlines = [HostIPInline]
|
||||||
|
|
||||||
|
# Select host to change -> Host
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Host"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class HostIpResource(resources.ModelResource):
|
class HostIpResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -793,6 +851,14 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||||
resource_classes = [HostIpResource]
|
resource_classes = [HostIpResource]
|
||||||
model = models.HostIP
|
model = models.HostIP
|
||||||
|
|
||||||
|
# Select host ip to change -> Host ip
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Host IP"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class ContactResource(resources.ModelResource):
|
class ContactResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -926,6 +992,14 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||||
|
|
||||||
|
# Select contact to change -> Contacts
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Contacts"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class WebsiteResource(resources.ModelResource):
|
class WebsiteResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -1043,6 +1117,21 @@ class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
else:
|
else:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
# User Domain manager [email] is manager on domain [domain name] ->
|
||||||
|
# Domain manager [email] on [domain name]
|
||||||
|
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
|
||||||
|
if object_id:
|
||||||
|
obj = self.get_object(request, object_id)
|
||||||
|
if obj:
|
||||||
|
email = obj.user.email
|
||||||
|
domain_name = obj.domain.name
|
||||||
|
extra_context["subtitle"] = f"Domain manager {email} on {domain_name}"
|
||||||
|
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitationAdmin(ListHeaderAdmin):
|
class DomainInvitationAdmin(ListHeaderAdmin):
|
||||||
"""Custom domain invitation admin class."""
|
"""Custom domain invitation admin class."""
|
||||||
|
@ -1079,6 +1168,14 @@ class DomainInvitationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||||
|
|
||||||
|
# Select domain invitations to change -> Domain invitations
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Domain invitations"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class DomainInformationResource(resources.ModelResource):
|
class DomainInformationResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -1219,6 +1316,14 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
return readonly_fields # Read-only fields for analysts
|
return readonly_fields # Read-only fields for analysts
|
||||||
|
|
||||||
|
# Select domain information to change -> Domain information
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Domain information"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestResource(FsmModelResource):
|
class DomainRequestResource(FsmModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -1674,11 +1779,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if next_char.isdigit():
|
if next_char.isdigit():
|
||||||
should_apply_default_filter = True
|
should_apply_default_filter = True
|
||||||
|
|
||||||
|
# Select domain request to change -> Domain requests
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Domain requests"
|
||||||
|
|
||||||
if should_apply_default_filter:
|
if should_apply_default_filter:
|
||||||
# modify the GET of the request to set the selected filter
|
# modify the GET of the request to set the selected filter
|
||||||
modified_get = copy.deepcopy(request.GET)
|
modified_get = copy.deepcopy(request.GET)
|
||||||
modified_get["status__in"] = "submitted,in review,action needed"
|
modified_get["status__in"] = "submitted,in review,action needed"
|
||||||
request.GET = modified_get
|
request.GET = modified_get
|
||||||
|
|
||||||
response = super().changelist_view(request, extra_context=extra_context)
|
response = super().changelist_view(request, extra_context=extra_context)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -2244,6 +2355,14 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# If no redirection is needed, return the original response
|
# If no redirection is needed, return the original response
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
# Select draft domain to change -> Draft domains
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "Draft domains"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class PublicContactResource(resources.ModelResource):
|
class PublicContactResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -2316,6 +2435,20 @@ class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||||
autocomplete_fields = ["domain"]
|
autocomplete_fields = ["domain"]
|
||||||
|
|
||||||
|
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
|
||||||
|
if object_id:
|
||||||
|
obj = self.get_object(request, object_id)
|
||||||
|
if obj:
|
||||||
|
name = obj.name
|
||||||
|
email = obj.email
|
||||||
|
registry_id = obj.registry_id
|
||||||
|
extra_context["subtitle"] = f"{name} <{email}> id: {registry_id}"
|
||||||
|
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class VerifiedByStaffAdmin(ListHeaderAdmin):
|
class VerifiedByStaffAdmin(ListHeaderAdmin):
|
||||||
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||||
|
@ -2368,8 +2501,18 @@ class UserGroupAdmin(AuditedAdmin):
|
||||||
def user_group(self, obj):
|
def user_group(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
|
||||||
|
# Select user groups to change -> User groups
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context["tabtitle"] = "User groups"
|
||||||
|
# Get the filtered values
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class WaffleFlagAdmin(FlagAdmin):
|
class WaffleFlagAdmin(FlagAdmin):
|
||||||
|
"""Custom admin implementation of django-waffle's Flag class"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Contains meta information about this class"""
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
@ -2403,6 +2546,6 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||||
# Register our custom waffle implementations
|
# Register our custom waffle implementations
|
||||||
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
||||||
|
|
||||||
# Unregister Sample and Switch from the waffle library
|
# Unregister Switch and Sample from the waffle library
|
||||||
admin.site.unregister(Sample)
|
|
||||||
admin.site.unregister(Switch)
|
admin.site.unregister(Switch)
|
||||||
|
admin.site.unregister(Sample)
|
||||||
|
|
|
@ -834,3 +834,589 @@ function hideDeletedForms() {
|
||||||
(function cisaRepresentativesFormListener() {
|
(function cisaRepresentativesFormListener() {
|
||||||
HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null)
|
HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null)
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize USWDS tooltips by calling initialization method. Requires that uswds-edited.js
|
||||||
|
* be loaded before get-gov.js. uswds-edited.js adds the tooltip module to the window to be
|
||||||
|
* accessible directly in get-gov.js
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function initializeTooltips() {
|
||||||
|
function checkTooltip() {
|
||||||
|
// Check that the tooltip library is loaded, and if not, wait and retry
|
||||||
|
if (window.tooltip && typeof window.tooltip.init === 'function') {
|
||||||
|
window.tooltip.init();
|
||||||
|
} else {
|
||||||
|
// Retry after a short delay
|
||||||
|
setTimeout(checkTooltip, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize USWDS modals by calling on method. Requires that uswds-edited.js be loaded
|
||||||
|
* before get-gov.js. uswds-edited.js adds the modal module to the window to be accessible
|
||||||
|
* directly in get-gov.js.
|
||||||
|
* initializeModals adds modal-related DOM elements, based on other DOM elements existing in
|
||||||
|
* the page. It needs to be called only once for any particular DOM element; otherwise, it
|
||||||
|
* will initialize improperly. Therefore, if DOM elements change dynamically and include
|
||||||
|
* DOM elements with modal classes, unloadModals needs to be called before initializeModals.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function initializeModals() {
|
||||||
|
window.modal.on();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload existing USWDS modals by calling off method. Requires that uswds-edited.js be
|
||||||
|
* loaded before get-gov.js. uswds-edited.js adds the modal module to the window to be
|
||||||
|
* accessible directly in get-gov.js.
|
||||||
|
* See note above with regards to calling this method relative to initializeModals.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function unloadModals() {
|
||||||
|
window.modal.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that scrolls to an element
|
||||||
|
* @param {string} attributeName - The string "class" or "id"
|
||||||
|
* @param {string} attributeValue - The class or id name
|
||||||
|
*/
|
||||||
|
function ScrollToElement(attributeName, attributeValue) {
|
||||||
|
let targetEl = null;
|
||||||
|
|
||||||
|
if (attributeName === 'class') {
|
||||||
|
targetEl = document.getElementsByClassName(attributeValue)[0];
|
||||||
|
} else if (attributeName === 'id') {
|
||||||
|
targetEl = document.getElementById(attributeValue);
|
||||||
|
} else {
|
||||||
|
console.log('Error: unknown attribute name provided.');
|
||||||
|
return; // Exit the function if an invalid attributeName is provided
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetEl) {
|
||||||
|
const rect = targetEl.getBoundingClientRect();
|
||||||
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||||
|
window.scrollTo({
|
||||||
|
top: rect.top + scrollTop,
|
||||||
|
behavior: 'smooth' // Optional: for smooth scrolling
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generalized function to update pagination for a list.
|
||||||
|
* @param {string} itemName - The name displayed in the counter
|
||||||
|
* @param {string} paginationSelector - CSS selector for the pagination container.
|
||||||
|
* @param {string} counterSelector - CSS selector for the pagination counter.
|
||||||
|
* @param {string} headerAnchor - CSS selector for the header element to anchor the links to.
|
||||||
|
* @param {Function} loadPageFunction - Function to call when a page link is clicked.
|
||||||
|
* @param {number} currentPage - The current page number (starting with 1).
|
||||||
|
* @param {number} numPages - The total number of pages.
|
||||||
|
* @param {boolean} hasPrevious - Whether there is a page before the current page.
|
||||||
|
* @param {boolean} hasNext - Whether there is a page after the current page.
|
||||||
|
* @param {number} totalItems - The total number of items.
|
||||||
|
*/
|
||||||
|
function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems) {
|
||||||
|
const paginationContainer = document.querySelector(paginationSelector);
|
||||||
|
const paginationCounter = document.querySelector(counterSelector);
|
||||||
|
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
||||||
|
paginationCounter.innerHTML = '';
|
||||||
|
paginationButtons.innerHTML = '';
|
||||||
|
|
||||||
|
// Buttons should only be displayed if there are more than one pages of results
|
||||||
|
paginationButtons.classList.toggle('display-none', numPages <= 1);
|
||||||
|
|
||||||
|
// Counter should only be displayed if there is more than 1 item
|
||||||
|
paginationContainer.classList.toggle('display-none', totalItems < 1);
|
||||||
|
|
||||||
|
paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
if (hasPrevious) {
|
||||||
|
const prevPageItem = document.createElement('li');
|
||||||
|
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
|
prevPageItem.innerHTML = `
|
||||||
|
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
|
||||||
|
</svg>
|
||||||
|
<span class="usa-pagination__link-text">Previous</span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
prevPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
loadPageFunction(currentPage - 1);
|
||||||
|
});
|
||||||
|
paginationButtons.appendChild(prevPageItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a page item
|
||||||
|
function createPageItem(page) {
|
||||||
|
const pageItem = document.createElement('li');
|
||||||
|
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
||||||
|
pageItem.innerHTML = `
|
||||||
|
<a href="${headerAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||||
|
`;
|
||||||
|
if (page === currentPage) {
|
||||||
|
pageItem.querySelector('a').classList.add('usa-current');
|
||||||
|
pageItem.querySelector('a').setAttribute('aria-current', 'page');
|
||||||
|
}
|
||||||
|
pageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
loadPageFunction(page);
|
||||||
|
});
|
||||||
|
return pageItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add first page and ellipsis if necessary
|
||||||
|
if (currentPage > 2) {
|
||||||
|
paginationButtons.appendChild(createPageItem(1));
|
||||||
|
if (currentPage > 3) {
|
||||||
|
const ellipsis = document.createElement('li');
|
||||||
|
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
||||||
|
ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
|
||||||
|
ellipsis.innerHTML = '<span>…</span>';
|
||||||
|
paginationButtons.appendChild(ellipsis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pages around the current page
|
||||||
|
for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) {
|
||||||
|
paginationButtons.appendChild(createPageItem(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add last page and ellipsis if necessary
|
||||||
|
if (currentPage < numPages - 1) {
|
||||||
|
if (currentPage < numPages - 2) {
|
||||||
|
const ellipsis = document.createElement('li');
|
||||||
|
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
||||||
|
ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
|
||||||
|
ellipsis.innerHTML = '<span>…</span>';
|
||||||
|
paginationButtons.appendChild(ellipsis);
|
||||||
|
}
|
||||||
|
paginationButtons.appendChild(createPageItem(numPages));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNext) {
|
||||||
|
const nextPageItem = document.createElement('li');
|
||||||
|
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
|
nextPageItem.innerHTML = `
|
||||||
|
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
||||||
|
<span class="usa-pagination__link-text">Next</span>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
nextPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
loadPageFunction(currentPage + 1);
|
||||||
|
});
|
||||||
|
paginationButtons.appendChild(nextPageItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||||
|
* initializes the domains list and associated functionality on the home page of the app.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let domainsWrapper = document.querySelector('.domains-wrapper');
|
||||||
|
|
||||||
|
if (domainsWrapper) {
|
||||||
|
let currentSortBy = 'id';
|
||||||
|
let currentOrder = 'asc';
|
||||||
|
let noDomainsWrapper = document.querySelector('.no-domains-wrapper');
|
||||||
|
let hasLoaded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||||
|
* based on the supplied attributes.
|
||||||
|
* @param {*} page - the page number of the results (starts with 1)
|
||||||
|
* @param {*} sortBy - the sort column option
|
||||||
|
* @param {*} order - the sort order {asc, desc}
|
||||||
|
* @param {*} loaded - control for the scrollToElement functionality
|
||||||
|
*/
|
||||||
|
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded) {
|
||||||
|
//fetch json of page of domains, given page # and sort
|
||||||
|
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.log('Error in AJAX call: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the display of proper messaging in the event that no domains exist in the list
|
||||||
|
if (data.domains.length) {
|
||||||
|
domainsWrapper.classList.remove('display-none');
|
||||||
|
noDomainsWrapper.classList.add('display-none');
|
||||||
|
} else {
|
||||||
|
domainsWrapper.classList.add('display-none');
|
||||||
|
noDomainsWrapper.classList.remove('display-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
// identify the DOM element where the domain list will be inserted into the DOM
|
||||||
|
const domainList = document.querySelector('.dotgov-table__registered-domains tbody');
|
||||||
|
domainList.innerHTML = '';
|
||||||
|
|
||||||
|
data.domains.forEach(domain => {
|
||||||
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||||
|
const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null;
|
||||||
|
const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : null;
|
||||||
|
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
||||||
|
const actionUrl = domain.action_url;
|
||||||
|
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
|
${domain.name}
|
||||||
|
</th>
|
||||||
|
<td data-sort-value="${expirationDateSortValue}" data-label="Expires">
|
||||||
|
${expirationDateFormatted}
|
||||||
|
</td>
|
||||||
|
<td data-label="Status">
|
||||||
|
${domain.state_display}
|
||||||
|
<svg
|
||||||
|
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
|
||||||
|
data-position="top"
|
||||||
|
title="${domain.get_state_help_text}"
|
||||||
|
focusable="true"
|
||||||
|
aria-label="Status Information"
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="${actionUrl}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#${domain.svg_icon}"></use>
|
||||||
|
</svg>
|
||||||
|
${domain.action_label} <span class="usa-sr-only">${domain.name}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
domainList.appendChild(row);
|
||||||
|
});
|
||||||
|
// initialize tool tips immediately after the associated DOM elements are added
|
||||||
|
initializeTooltips();
|
||||||
|
if (loaded)
|
||||||
|
ScrollToElement('id', 'domains-header');
|
||||||
|
|
||||||
|
hasLoaded = true;
|
||||||
|
|
||||||
|
// update pagination
|
||||||
|
updatePagination(
|
||||||
|
'domain',
|
||||||
|
'#domains-pagination',
|
||||||
|
'#domains-pagination .usa-pagination__counter',
|
||||||
|
'#domains-header',
|
||||||
|
loadDomains,
|
||||||
|
data.page,
|
||||||
|
data.num_pages,
|
||||||
|
data.has_previous,
|
||||||
|
data.has_next,
|
||||||
|
data.total
|
||||||
|
);
|
||||||
|
currentSortBy = sortBy;
|
||||||
|
currentOrder = order;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching domains:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Add event listeners to table headers for sorting
|
||||||
|
document.querySelectorAll('.dotgov-table__registered-domains th[data-sortable]').forEach(header => {
|
||||||
|
header.addEventListener('click', function() {
|
||||||
|
const sortBy = this.getAttribute('data-sortable');
|
||||||
|
let order = 'asc';
|
||||||
|
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||||
|
// is selecting the same column to sort in descending order
|
||||||
|
if (sortBy === currentSortBy) {
|
||||||
|
order = currentOrder === 'asc' ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
// load the results with the updated sort
|
||||||
|
loadDomains(1, sortBy, order);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the first page initially
|
||||||
|
loadDomains(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const utcDateString = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const utcYear = date.getUTCFullYear();
|
||||||
|
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
|
||||||
|
const utcDay = date.getUTCDate().toString().padStart(2, '0');
|
||||||
|
const utcHours = date.getUTCHours().toString().padStart(2, '0');
|
||||||
|
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} UTC`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||||
|
* initializes the domain requests list and associated functionality on the home page of the app.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let domainRequestsWrapper = document.querySelector('.domain-requests-wrapper');
|
||||||
|
|
||||||
|
if (domainRequestsWrapper) {
|
||||||
|
let currentSortBy = 'id';
|
||||||
|
let currentOrder = 'asc';
|
||||||
|
let noDomainRequestsWrapper = document.querySelector('.no-domain-requests-wrapper');
|
||||||
|
let hasLoaded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads rows in the domain requests list, as well as updates pagination around the domain requests list
|
||||||
|
* based on the supplied attributes.
|
||||||
|
* @param {*} page - the page number of the results (starts with 1)
|
||||||
|
* @param {*} sortBy - the sort column option
|
||||||
|
* @param {*} order - the sort order {asc, desc}
|
||||||
|
* @param {*} loaded - control for the scrollToElement functionality
|
||||||
|
*/
|
||||||
|
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded) {
|
||||||
|
//fetch json of page of domain requests, given page # and sort
|
||||||
|
fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.log('Error in AJAX call: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the display of proper messaging in the event that no domain requests exist in the list
|
||||||
|
if (data.domain_requests.length) {
|
||||||
|
domainRequestsWrapper.classList.remove('display-none');
|
||||||
|
noDomainRequestsWrapper.classList.add('display-none');
|
||||||
|
} else {
|
||||||
|
domainRequestsWrapper.classList.add('display-none');
|
||||||
|
noDomainRequestsWrapper.classList.remove('display-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
// identify the DOM element where the domain request list will be inserted into the DOM
|
||||||
|
const tbody = document.querySelector('.dotgov-table__domain-requests tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
// remove any existing modal elements from the DOM so they can be properly re-initialized
|
||||||
|
// after the DOM content changes and there are new delete modal buttons added
|
||||||
|
unloadModals();
|
||||||
|
data.domain_requests.forEach(request => {
|
||||||
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||||
|
const domainName = request.requested_domain ? request.requested_domain : `New domain request <br><span class="text-base font-body-xs">(${utcDateString(request.created_at)})</span>`;
|
||||||
|
const actionUrl = request.action_url;
|
||||||
|
const actionLabel = request.action_label;
|
||||||
|
const submissionDate = request.submission_date ? new Date(request.submission_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
||||||
|
const deleteButton = request.is_deletable ? `
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
|
class="usa-button--unstyled text-no-underline late-loading-modal-trigger"
|
||||||
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
|
data-open-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
|
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||||
|
</a>` : '';
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
|
${domainName}
|
||||||
|
</th>
|
||||||
|
<td data-sort-value="${new Date(request.submission_date).getTime()}" data-label="Date submitted">
|
||||||
|
${submissionDate}
|
||||||
|
</td>
|
||||||
|
<td data-label="Status">
|
||||||
|
${request.status}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="${actionUrl}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#${request.svg_icon}"></use>
|
||||||
|
</svg>
|
||||||
|
${actionLabel} <span class="usa-sr-only">${request.requested_domain ? request.requested_domain : 'New domain request'}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>${deleteButton}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
// initialize modals immediately after the DOM content is updated
|
||||||
|
initializeModals();
|
||||||
|
if (loaded)
|
||||||
|
ScrollToElement('id', 'domain-requests-header');
|
||||||
|
|
||||||
|
hasLoaded = true;
|
||||||
|
|
||||||
|
// update the pagination after the domain requests list is updated
|
||||||
|
updatePagination(
|
||||||
|
'domain request',
|
||||||
|
'#domain-requests-pagination',
|
||||||
|
'#domain-requests-pagination .usa-pagination__counter',
|
||||||
|
'#domain-requests-header',
|
||||||
|
loadDomainRequests,
|
||||||
|
data.page,
|
||||||
|
data.num_pages,
|
||||||
|
data.has_previous,
|
||||||
|
data.has_next,
|
||||||
|
data.total
|
||||||
|
);
|
||||||
|
currentSortBy = sortBy;
|
||||||
|
currentOrder = order;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching domain requests:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners to table headers for sorting
|
||||||
|
document.querySelectorAll('.dotgov-table__domain-requests th[data-sortable]').forEach(header => {
|
||||||
|
header.addEventListener('click', function() {
|
||||||
|
const sortBy = this.getAttribute('data-sortable');
|
||||||
|
let order = 'asc';
|
||||||
|
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||||
|
// is selecting the same column to sort in descending order
|
||||||
|
if (sortBy === currentSortBy) {
|
||||||
|
order = currentOrder === 'asc' ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
loadDomainRequests(1, sortBy, order);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the first page initially
|
||||||
|
loadDomainRequests(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An IIFE that hooks up the edit buttons on the finish-user-setup page
|
||||||
|
*/
|
||||||
|
(function finishUserSetupListener() {
|
||||||
|
|
||||||
|
function getInputField(fieldName){
|
||||||
|
return document.querySelector(`#id_${fieldName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows the hidden input field and hides the readonly one
|
||||||
|
function showInputFieldHideReadonlyField(fieldName, button) {
|
||||||
|
let inputField = getInputField(fieldName)
|
||||||
|
let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`)
|
||||||
|
|
||||||
|
readonlyField.classList.toggle('display-none');
|
||||||
|
inputField.classList.toggle('display-none');
|
||||||
|
|
||||||
|
// Toggle the bold style on the grid row
|
||||||
|
let gridRow = button.closest(".grid-col-2").closest(".grid-row")
|
||||||
|
if (gridRow){
|
||||||
|
gridRow.classList.toggle("bold-usa-label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFullNameField(fieldName = "full_name") {
|
||||||
|
// Remove the display-none class from the nearest parent div
|
||||||
|
let nameFieldset = document.querySelector("#profile-name-group");
|
||||||
|
if (nameFieldset){
|
||||||
|
nameFieldset.classList.remove("display-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the "full_name" field
|
||||||
|
let inputField = getInputField(fieldName);
|
||||||
|
if (inputField) {
|
||||||
|
inputFieldParentDiv = inputField.closest("div");
|
||||||
|
if (inputFieldParentDiv) {
|
||||||
|
inputFieldParentDiv.classList.add("display-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditButtonClick(fieldName, button){
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
// Lock the edit button while this operation occurs
|
||||||
|
button.disabled = true
|
||||||
|
|
||||||
|
if (fieldName == "full_name"){
|
||||||
|
handleFullNameField();
|
||||||
|
}else {
|
||||||
|
showInputFieldHideReadonlyField(fieldName, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the button itself
|
||||||
|
button.classList.add("display-none");
|
||||||
|
|
||||||
|
// Unlock after it completes
|
||||||
|
button.disabled = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupListener(){
|
||||||
|
document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) {
|
||||||
|
// Get the "{field_name}" and "edit-button"
|
||||||
|
let fieldIdParts = button.id.split("__")
|
||||||
|
if (fieldIdParts && fieldIdParts.length > 0){
|
||||||
|
let fieldName = fieldIdParts[0]
|
||||||
|
|
||||||
|
// When the edit button is clicked, show the input field under it
|
||||||
|
handleEditButtonClick(fieldName, button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInputOnErrorFields(){
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Get all input elements within the form
|
||||||
|
let form = document.querySelector("#finish-profile-setup-form");
|
||||||
|
let inputs = form ? form.querySelectorAll("input") : null;
|
||||||
|
if (!inputs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullNameButtonClicked = false
|
||||||
|
inputs.forEach(function(input) {
|
||||||
|
let fieldName = input.name;
|
||||||
|
let errorMessage = document.querySelector(`#id_${fieldName}__error-message`);
|
||||||
|
|
||||||
|
// If no error message is found, do nothing
|
||||||
|
if (!fieldName || !errorMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editButton = document.querySelector(`#${fieldName}__edit-button`);
|
||||||
|
if (editButton){
|
||||||
|
// Show the input field of the field that errored out
|
||||||
|
editButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If either the full_name field errors out,
|
||||||
|
// or if any of its associated fields do - show all name related fields.
|
||||||
|
let nameFields = ["first_name", "middle_name", "last_name"];
|
||||||
|
if (nameFields.includes(fieldName) && !fullNameButtonClicked){
|
||||||
|
// Click the full name button if any of its related fields error out
|
||||||
|
fullNameButton = document.querySelector("#full_name__edit-button");
|
||||||
|
if (fullNameButton) {
|
||||||
|
fullNameButton.click();
|
||||||
|
fullNameButtonClicked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hookup all edit buttons to the `handleEditButtonClick` function
|
||||||
|
setupListener();
|
||||||
|
|
||||||
|
// Show the input fields if an error exists
|
||||||
|
showInputOnErrorFields();
|
||||||
|
})();
|
||||||
|
|
7042
src/registrar/assets/js/uswds-edited.js
Normal file
7042
src/registrar/assets/js/uswds-edited.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -130,7 +130,7 @@ html[data-theme="light"] {
|
||||||
// Sets darker color on delete page links.
|
// Sets darker color on delete page links.
|
||||||
// Remove when dark mode successfully applies to Django delete page.
|
// Remove when dark mode successfully applies to Django delete page.
|
||||||
.delete-confirmation .content a:not(.button) {
|
.delete-confirmation .content a:not(.button) {
|
||||||
color: #005288;
|
color: color('primary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,10 +159,11 @@ html[data-theme="dark"] {
|
||||||
// Sets darker color on delete page links.
|
// Sets darker color on delete page links.
|
||||||
// Remove when dark mode successfully applies to Django delete page.
|
// Remove when dark mode successfully applies to Django delete page.
|
||||||
.delete-confirmation .content a:not(.button) {
|
.delete-confirmation .content a:not(.button) {
|
||||||
color: #005288;
|
color: color('primary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#branding h1 a:link, #branding h1 a:visited {
|
#branding h1 a:link, #branding h1 a:visited {
|
||||||
color: var(--primary-fg);
|
color: var(--primary-fg);
|
||||||
}
|
}
|
||||||
|
@ -185,6 +186,14 @@ div#content > h2 {
|
||||||
margin: units(2) 0 units(1) 0;
|
margin: units(2) 0 units(1) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module ul.padding-0 {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module ul.margin-0 {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.change-list {
|
.change-list {
|
||||||
.usa-table--striped tbody tr:nth-child(odd) td,
|
.usa-table--striped tbody tr:nth-child(odd) td,
|
||||||
.usa-table--striped tbody tr:nth-child(odd) th,
|
.usa-table--striped tbody tr:nth-child(odd) th,
|
||||||
|
@ -194,6 +203,18 @@ div#content > h2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.change-form {
|
||||||
|
.usa-table--striped tbody tr:nth-child(odd) td,
|
||||||
|
.usa-table--striped tbody tr:nth-child(odd) th,
|
||||||
|
.usa-table td,
|
||||||
|
.usa-table th {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.usa-table td {
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#nav-sidebar {
|
#nav-sidebar {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
@ -719,7 +740,7 @@ div.dja__model-description{
|
||||||
|
|
||||||
a, a:link, a:visited {
|
a, a:link, a:visited {
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
color: #005288 !important;
|
color: color('primary') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dja__model-description--no-overflow {
|
&.dja__model-description--no-overflow {
|
||||||
|
@ -748,3 +769,7 @@ div.dja__model-description{
|
||||||
.usa-summary-box h3 {
|
.usa-summary-box h3 {
|
||||||
color: #{$dhs-blue-60};
|
color: #{$dhs-blue-60};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module caption, .inline-group h2 {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
|
@use "cisa_colors" as *;
|
||||||
|
|
||||||
/* Styles for making visible to screen reader / AT users only. */
|
/* Styles for making visible to screen reader / AT users only. */
|
||||||
.sr-only {
|
.sr-only {
|
||||||
|
@ -169,3 +170,44 @@ abbr[title] {
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-with-edit-button {
|
||||||
|
svg.usa-icon {
|
||||||
|
width: 1.5em !important;
|
||||||
|
height: 1.5em !important;
|
||||||
|
color: #{$dhs-green};
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
&.input-with-edit-button__error {
|
||||||
|
svg.usa-icon {
|
||||||
|
color: #{$dhs-red};
|
||||||
|
}
|
||||||
|
div.readonly-field {
|
||||||
|
color: #{$dhs-red};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to deviate from some default USWDS styles here
|
||||||
|
// in this particular case, so we have to override this.
|
||||||
|
.usa-form .usa-button.readonly-edit-button {
|
||||||
|
margin-top: 0px !important;
|
||||||
|
padding-top: 0px !important;
|
||||||
|
svg {
|
||||||
|
width: 1.25em !important;
|
||||||
|
height: 1.25em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define some styles for the .gov header/logo
|
||||||
|
.usa-logo button {
|
||||||
|
color: #{$dhs-dark-gray-85};
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: family('sans');
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-logo button.usa-button--unstyled.disabled-button:hover{
|
||||||
|
color: #{$dhs-dark-gray-85};
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
|
@use "cisa_colors" as *;
|
||||||
|
|
||||||
/* Make "placeholder" links visually obvious */
|
/* Make "placeholder" links visually obvious */
|
||||||
a[href$="todo"]::after {
|
a[href$="todo"]::after {
|
||||||
|
@ -7,11 +8,16 @@ a[href$="todo"]::after {
|
||||||
content: " [link TBD]";
|
content: " [link TBD]";
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.usa-link.usa-link--always-blue {
|
||||||
|
color: #{$dhs-blue};
|
||||||
|
}
|
||||||
|
|
||||||
a.breadcrumb__back {
|
a.breadcrumb__back {
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: units(2.5);
|
margin-bottom: units(2.5);
|
||||||
|
color: #{$dhs-blue};
|
||||||
&:visited {
|
&:visited {
|
||||||
color: color('primary');
|
color: color('primary');
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ $dhs-gray-10: #fcfdfd;
|
||||||
|
|
||||||
/*--- Dark Gray ---*/
|
/*--- Dark Gray ---*/
|
||||||
$dhs-dark-gray-90: #040404;
|
$dhs-dark-gray-90: #040404;
|
||||||
|
$dhs-dark-gray-85: #1b1b1b;
|
||||||
$dhs-dark-gray-80: #19191a;
|
$dhs-dark-gray-80: #19191a;
|
||||||
$dhs-dark-gray-70: #2f2f30;
|
$dhs-dark-gray-70: #2f2f30;
|
||||||
$dhs-dark-gray-60: #444547;
|
$dhs-dark-gray-60: #444547;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
|
@use "cisa_colors" as *;
|
||||||
|
|
||||||
.usa-form .usa-button {
|
.usa-form .usa-button {
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
|
@ -26,6 +27,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-form-editable {
|
||||||
|
border-top: 2px #{$dhs-dark-gray-15} solid;
|
||||||
|
|
||||||
|
.bold-usa-label label.usa-label{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bold-usa-label label.usa-label{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.usa-form-editable--no-border {
|
||||||
|
border-top: None;
|
||||||
|
margin-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-form-editable > .usa-form-group:first-of-type {
|
||||||
|
margin-top: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 35em) {
|
||||||
|
.usa-form--largest {
|
||||||
|
max-width: 35rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.usa-form-group--unstyled-error {
|
.usa-form-group--unstyled-error {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
@ -52,4 +81,4 @@ legend.float-left-tablet + button.float-right-tablet {
|
||||||
background-color: var(--body-fg);
|
background-color: var(--body-fg);
|
||||||
color: var(--close-button-hover-bg);
|
color: var(--close-button-hover-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
src/registrar/assets/sass/_theme/_pagination.scss
Normal file
15
src/registrar/assets/sass/_theme/_pagination.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
|
.usa-pagination {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background-color: transparent;
|
||||||
|
.usa-current {
|
||||||
|
background-color: color('base-dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include at-media(desktop) {
|
||||||
|
.usa-pagination {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,3 +24,7 @@
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#extended-logo .usa-tooltip__body {
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
@forward "links";
|
@forward "links";
|
||||||
@forward "lists";
|
@forward "lists";
|
||||||
@forward "buttons";
|
@forward "buttons";
|
||||||
|
@forward "pagination";
|
||||||
@forward "forms";
|
@forward "forms";
|
||||||
@forward "tooltips";
|
@forward "tooltips";
|
||||||
@forward "fieldsets";
|
@forward "fieldsets";
|
||||||
|
|
|
@ -162,7 +162,7 @@ MIDDLEWARE = [
|
||||||
# django-cors-headers: listen to cors responses
|
# django-cors-headers: listen to cors responses
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
# custom middleware to stop caching from CloudFront
|
# custom middleware to stop caching from CloudFront
|
||||||
"registrar.no_cache_middleware.NoCacheMiddleware",
|
"registrar.registrar_middleware.NoCacheMiddleware",
|
||||||
# serve static assets in production
|
# serve static assets in production
|
||||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
# provide security enhancements to the request/response cycle
|
# provide security enhancements to the request/response cycle
|
||||||
|
@ -188,6 +188,7 @@ MIDDLEWARE = [
|
||||||
"auditlog.middleware.AuditlogMiddleware",
|
"auditlog.middleware.AuditlogMiddleware",
|
||||||
# Used for waffle feature flags
|
# Used for waffle feature flags
|
||||||
"waffle.middleware.WaffleMiddleware",
|
"waffle.middleware.WaffleMiddleware",
|
||||||
|
"registrar.registrar_middleware.CheckUserProfileMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
# application object used by Django’s built-in servers (e.g. `runserver`)
|
# application object used by Django’s built-in servers (e.g. `runserver`)
|
||||||
|
@ -326,7 +327,7 @@ SERVER_EMAIL = "root@get.gov"
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Waffle feature flags-----------------------------------------------------------###
|
# region: Waffle feature flags-----------------------------------------------------------###
|
||||||
# If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag?
|
# If Waffle encounters a reference to a flag that is not in the database, create the flag automagically.
|
||||||
WAFFLE_CREATE_MISSING_FLAGS = True
|
WAFFLE_CREATE_MISSING_FLAGS = True
|
||||||
|
|
||||||
# The model that will be used to keep track of flags. Extends AbstractUserFlag.
|
# The model that will be used to keep track of flags. Extends AbstractUserFlag.
|
||||||
|
@ -658,6 +659,8 @@ ALLOWED_HOSTS = [
|
||||||
"getgov-stable.app.cloud.gov",
|
"getgov-stable.app.cloud.gov",
|
||||||
"getgov-staging.app.cloud.gov",
|
"getgov-staging.app.cloud.gov",
|
||||||
"getgov-development.app.cloud.gov",
|
"getgov-development.app.cloud.gov",
|
||||||
|
"getgov-litterbox.app.cloud.gov",
|
||||||
|
"getgov-hotgov.app.cloud.gov",
|
||||||
"getgov-cb.app.cloud.gov",
|
"getgov-cb.app.cloud.gov",
|
||||||
"getgov-bob.app.cloud.gov",
|
"getgov-bob.app.cloud.gov",
|
||||||
"getgov-meoward.app.cloud.gov",
|
"getgov-meoward.app.cloud.gov",
|
||||||
|
|
|
@ -21,6 +21,8 @@ from registrar.views.admin_views import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from registrar.views.domain_request import Step
|
from registrar.views.domain_request import Step
|
||||||
|
from registrar.views.domain_requests_json import get_domain_requests_json
|
||||||
|
from registrar.views.domains_json import get_domains_json
|
||||||
from registrar.views.utility import always_404
|
from registrar.views.utility import always_404
|
||||||
from api.views import available, get_current_federal, get_current_full
|
from api.views import available, get_current_federal, get_current_full
|
||||||
|
|
||||||
|
@ -178,6 +180,11 @@ urlpatterns = [
|
||||||
views.DomainAddUserView.as_view(),
|
views.DomainAddUserView.as_view(),
|
||||||
name="domain-users-add",
|
name="domain-users-add",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"finish-profile-setup",
|
||||||
|
views.FinishProfileSetupView.as_view(),
|
||||||
|
name="finish-user-profile-setup",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"user-profile",
|
"user-profile",
|
||||||
views.UserProfileView.as_view(),
|
views.UserProfileView.as_view(),
|
||||||
|
@ -198,6 +205,8 @@ urlpatterns = [
|
||||||
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
|
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
|
||||||
name="domain-user-delete",
|
name="domain-user-delete",
|
||||||
),
|
),
|
||||||
|
path("get-domains-json/", get_domains_json, name="get_domains_json"),
|
||||||
|
path("get-domain-requests-json/", get_domain_requests_json, name="get_domain_requests_json"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Djangooidc strips out context data from that context, so we define a custom error
|
# Djangooidc strips out context data from that context, so we define a custom error
|
||||||
|
|
|
@ -60,4 +60,35 @@ class UserProfileForm(forms.ModelForm):
|
||||||
}
|
}
|
||||||
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||||
|
|
||||||
|
if self.instance and self.instance.phone:
|
||||||
|
self.fields["phone"].initial = self.instance.phone.as_national
|
||||||
|
|
||||||
DomainHelper.disable_field(self.fields["email"], disable_required=True)
|
DomainHelper.disable_field(self.fields["email"], disable_required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FinishSetupProfileForm(UserProfileForm):
|
||||||
|
"""Form for updating user profile."""
|
||||||
|
|
||||||
|
full_name = forms.CharField(required=True, label="Full name")
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
# Remove the full name property
|
||||||
|
if "full_name" in cleaned_data:
|
||||||
|
# Delete the full name element as its purely decorative.
|
||||||
|
# We include it as a normal Charfield for all the advantages
|
||||||
|
# and utility that it brings, but we're playing pretend.
|
||||||
|
del cleaned_data["full_name"]
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Override the inerited __init__ method to update the fields."""
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Set custom form label for email
|
||||||
|
self.fields["email"].label = "Organization email"
|
||||||
|
self.fields["title"].label = "Title or role in your organization"
|
||||||
|
|
||||||
|
# Define the "full_name" value
|
||||||
|
self.fields["full_name"].initial = self.instance.get_formatted_name()
|
||||||
|
|
|
@ -10,6 +10,7 @@ from registrar.management.commands.utility.terminal_helper import (
|
||||||
)
|
)
|
||||||
from registrar.models.contact import Contact
|
from registrar.models.contact import Contact
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
|
from registrar.models.utility.domain_helper import DomainHelper
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -110,15 +111,21 @@ class Command(BaseCommand):
|
||||||
{TerminalColors.ENDC}""", # noqa
|
{TerminalColors.ENDC}""", # noqa
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- UPDATE THE USER IF IT DOES NOT HAVE A FIRST AND LAST NAMES
|
# Get the fields that exist on both User and Contact. Excludes id.
|
||||||
# ---- LET'S KEEP A LIGHT TOUCH
|
common_fields = DomainHelper.get_common_fields(User, Contact)
|
||||||
if not eligible_user.first_name and not eligible_user.last_name:
|
if "email" in common_fields:
|
||||||
# (expression has type "str | None", variable has type "str | int | Combinable")
|
# Don't change the email field.
|
||||||
# so we'll ignore type
|
common_fields.remove("email")
|
||||||
eligible_user.first_name = contact.first_name # type: ignore
|
|
||||||
eligible_user.last_name = contact.last_name # type: ignore
|
for field in common_fields:
|
||||||
eligible_user.save()
|
# Grab the value that contact has stored for this field
|
||||||
processed_user = eligible_user
|
new_value = getattr(contact, field)
|
||||||
|
|
||||||
|
# Set it on the user field
|
||||||
|
setattr(eligible_user, field, new_value)
|
||||||
|
|
||||||
|
eligible_user.save()
|
||||||
|
processed_user = eligible_user
|
||||||
|
|
||||||
return (
|
return (
|
||||||
eligible_user,
|
eligible_user,
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-05-28 14:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0095_user_middle_name_user_title"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="email",
|
||||||
|
field=models.EmailField(blank=True, max_length=320, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="first_name",
|
||||||
|
field=models.CharField(blank=True, null=True, verbose_name="first name"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="last_name",
|
||||||
|
field=models.CharField(blank=True, null=True, verbose_name="last name"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="phone",
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="organization_name",
|
||||||
|
field=models.CharField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="zipcode",
|
||||||
|
field=models.CharField(blank=True, max_length=10, null=True, verbose_name="zip code"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="organization_name",
|
||||||
|
field=models.CharField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="zipcode",
|
||||||
|
field=models.CharField(blank=True, max_length=10, null=True, verbose_name="zip code"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="first_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, help_text="First name / given name", null=True, verbose_name="first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="organization_name",
|
||||||
|
field=models.CharField(blank=True, help_text="Organization name", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="zipcode",
|
||||||
|
field=models.CharField(blank=True, help_text="Zip code", max_length=10, null=True, verbose_name="zip code"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="phone",
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(
|
||||||
|
blank=True, help_text="Phone", max_length=128, null=True, region=None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="verifiedbystaff",
|
||||||
|
name="email",
|
||||||
|
field=models.EmailField(max_length=254),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="contact",
|
||||||
|
index=models.Index(fields=["user"], name="registrar_c_user_id_4059c4_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="contact",
|
||||||
|
index=models.Index(fields=["email"], name="registrar_c_email_bde2de_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domain",
|
||||||
|
index=models.Index(fields=["name"], name="registrar_d_name_5b1956_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domain",
|
||||||
|
index=models.Index(fields=["state"], name="registrar_d_state_84c134_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domaininformation",
|
||||||
|
index=models.Index(fields=["domain"], name="registrar_d_domain__88838a_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domaininformation",
|
||||||
|
index=models.Index(fields=["domain_request"], name="registrar_d_domain__d1fba8_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domaininvitation",
|
||||||
|
index=models.Index(fields=["status"], name="registrar_d_status_e84571_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domainrequest",
|
||||||
|
index=models.Index(fields=["requested_domain"], name="registrar_d_request_6894eb_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domainrequest",
|
||||||
|
index=models.Index(fields=["approved_domain"], name="registrar_d_approve_ac4c46_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="domainrequest",
|
||||||
|
index=models.Index(fields=["status"], name="registrar_d_status_a32b59_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="user",
|
||||||
|
index=models.Index(fields=["username"], name="registrar_u_usernam_964b1b_idx"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="user",
|
||||||
|
index=models.Index(fields=["email"], name="registrar_u_email_c8f2c4_idx"),
|
||||||
|
),
|
||||||
|
]
|
19
src/registrar/migrations/0097_alter_user_phone.py
Normal file
19
src/registrar/migrations/0097_alter_user_phone.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-06-06 18:38
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0096_alter_contact_email_alter_contact_first_name_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="phone",
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None),
|
||||||
|
),
|
||||||
|
]
|
32
src/registrar/migrations/0098_alter_domainrequest_status.py
Normal file
32
src/registrar/migrations/0098_alter_domainrequest_status.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-06-07 15:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django_fsm
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0097_alter_user_phone"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="status",
|
||||||
|
field=django_fsm.FSMField(
|
||||||
|
choices=[
|
||||||
|
("in review", "In review"),
|
||||||
|
("action needed", "Action needed"),
|
||||||
|
("approved", "Approved"),
|
||||||
|
("rejected", "Rejected"),
|
||||||
|
("ineligible", "Ineligible"),
|
||||||
|
("submitted", "Submitted"),
|
||||||
|
("withdrawn", "Withdrawn"),
|
||||||
|
("started", "Started"),
|
||||||
|
],
|
||||||
|
default="started",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -17,6 +17,14 @@ class Contact(TimeStampedModel):
|
||||||
will be updated if any updates are made to it through Login.gov.
|
will be updated if any updates are made to it through Login.gov.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["user"]),
|
||||||
|
models.Index(fields=["email"]),
|
||||||
|
]
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -28,7 +36,6 @@ class Contact(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="first name",
|
verbose_name="first name",
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
middle_name = models.CharField(
|
middle_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -38,7 +45,6 @@ class Contact(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="last name",
|
verbose_name="last name",
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -48,13 +54,11 @@ class Contact(TimeStampedModel):
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
|
||||||
max_length=320,
|
max_length=320,
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_all_relations(self):
|
def _get_all_relations(self):
|
||||||
|
@ -119,11 +123,21 @@ class Contact(TimeStampedModel):
|
||||||
self.user.last_name = self.last_name
|
self.user.last_name = self.last_name
|
||||||
updated = True
|
updated = True
|
||||||
|
|
||||||
|
# Update middle_name if necessary
|
||||||
|
if not self.user.middle_name:
|
||||||
|
self.user.middle_name = self.middle_name
|
||||||
|
updated = True
|
||||||
|
|
||||||
# Update phone if necessary
|
# Update phone if necessary
|
||||||
if not self.user.phone:
|
if not self.user.phone:
|
||||||
self.user.phone = self.phone
|
self.user.phone = self.phone
|
||||||
updated = True
|
updated = True
|
||||||
|
|
||||||
|
# Update title if necessary
|
||||||
|
if not self.user.title:
|
||||||
|
self.user.title = self.title
|
||||||
|
updated = True
|
||||||
|
|
||||||
# Save user if any updates were made
|
# Save user if any updates were made
|
||||||
if updated:
|
if updated:
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
|
@ -65,6 +65,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
domain meets the required checks.
|
domain meets the required checks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["name"]),
|
||||||
|
models.Index(fields=["state"]),
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
super(Domain, self).__init__(*args, **kwargs)
|
super(Domain, self).__init__(*args, **kwargs)
|
||||||
|
@ -1062,6 +1070,15 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
now = timezone.now().date()
|
now = timezone.now().date()
|
||||||
return self.expiration_date < now
|
return self.expiration_date < now
|
||||||
|
|
||||||
|
def state_display(self):
|
||||||
|
"""Return the display status of the domain."""
|
||||||
|
if self.is_expired() and self.state != self.State.UNKNOWN:
|
||||||
|
return "Expired"
|
||||||
|
elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED:
|
||||||
|
return "DNS needed"
|
||||||
|
else:
|
||||||
|
return self.state.capitalize()
|
||||||
|
|
||||||
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
|
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
|
||||||
"""Maps the Epp contact representation to a PublicContact object.
|
"""Maps the Epp contact representation to a PublicContact object.
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,16 @@ class DomainInformation(TimeStampedModel):
|
||||||
the domain request once approved, so copying them that way we can make changes
|
the domain request once approved, so copying them that way we can make changes
|
||||||
after its approved. Most fields here are copied from DomainRequest."""
|
after its approved. Most fields here are copied from DomainRequest."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["domain"]),
|
||||||
|
models.Index(fields=["domain_request"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
verbose_name_plural = "Domain information"
|
||||||
|
|
||||||
StateTerritoryChoices = DomainRequest.StateTerritoryChoices
|
StateTerritoryChoices = DomainRequest.StateTerritoryChoices
|
||||||
|
|
||||||
# use the short names in Django admin
|
# use the short names in Django admin
|
||||||
|
@ -111,7 +121,6 @@ class DomainInformation(TimeStampedModel):
|
||||||
organization_name = models.CharField(
|
organization_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
address_line1 = models.CharField(
|
address_line1 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -138,7 +147,6 @@ class DomainInformation(TimeStampedModel):
|
||||||
max_length=10,
|
max_length=10,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
|
||||||
verbose_name="zip code",
|
verbose_name="zip code",
|
||||||
)
|
)
|
||||||
urbanization = models.CharField(
|
urbanization = models.CharField(
|
||||||
|
@ -336,6 +344,3 @@ class DomainInformation(TimeStampedModel):
|
||||||
def _get_many_to_many_fields():
|
def _get_many_to_many_fields():
|
||||||
"""Returns a set of each field.name that has the many to many relation"""
|
"""Returns a set of each field.name that has the many to many relation"""
|
||||||
return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore
|
return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Domain information"
|
|
||||||
|
|
|
@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitation(TimeStampedModel):
|
class DomainInvitation(TimeStampedModel):
|
||||||
|
class Meta:
|
||||||
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["status"]),
|
||||||
|
]
|
||||||
|
|
||||||
# Constants for status field
|
# Constants for status field
|
||||||
class DomainInvitationStatus(models.TextChoices):
|
class DomainInvitationStatus(models.TextChoices):
|
||||||
INVITED = "invited", "Invited"
|
INVITED = "invited", "Invited"
|
||||||
|
|
|
@ -25,6 +25,15 @@ logger = logging.getLogger(__name__)
|
||||||
class DomainRequest(TimeStampedModel):
|
class DomainRequest(TimeStampedModel):
|
||||||
"""A registrant's domain request for a new domain."""
|
"""A registrant's domain request for a new domain."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["requested_domain"]),
|
||||||
|
models.Index(fields=["approved_domain"]),
|
||||||
|
models.Index(fields=["status"]),
|
||||||
|
]
|
||||||
|
|
||||||
# https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history
|
# https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history
|
||||||
# If we note any performace degradation due to this addition,
|
# If we note any performace degradation due to this addition,
|
||||||
# we can query the auditlogs table in admin.py and add the results to
|
# we can query the auditlogs table in admin.py and add the results to
|
||||||
|
@ -34,14 +43,14 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
# Constants for choice fields
|
# Constants for choice fields
|
||||||
class DomainRequestStatus(models.TextChoices):
|
class DomainRequestStatus(models.TextChoices):
|
||||||
STARTED = "started", "Started"
|
|
||||||
SUBMITTED = "submitted", "Submitted"
|
|
||||||
IN_REVIEW = "in review", "In review"
|
IN_REVIEW = "in review", "In review"
|
||||||
ACTION_NEEDED = "action needed", "Action needed"
|
ACTION_NEEDED = "action needed", "Action needed"
|
||||||
APPROVED = "approved", "Approved"
|
APPROVED = "approved", "Approved"
|
||||||
WITHDRAWN = "withdrawn", "Withdrawn"
|
|
||||||
REJECTED = "rejected", "Rejected"
|
REJECTED = "rejected", "Rejected"
|
||||||
INELIGIBLE = "ineligible", "Ineligible"
|
INELIGIBLE = "ineligible", "Ineligible"
|
||||||
|
SUBMITTED = "submitted", "Submitted"
|
||||||
|
WITHDRAWN = "withdrawn", "Withdrawn"
|
||||||
|
STARTED = "started", "Started"
|
||||||
|
|
||||||
class StateTerritoryChoices(models.TextChoices):
|
class StateTerritoryChoices(models.TextChoices):
|
||||||
ALABAMA = "AL", "Alabama (AL)"
|
ALABAMA = "AL", "Alabama (AL)"
|
||||||
|
@ -331,7 +340,6 @@ class DomainRequest(TimeStampedModel):
|
||||||
organization_name = models.CharField(
|
organization_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
address_line1 = models.CharField(
|
address_line1 = models.CharField(
|
||||||
|
@ -360,7 +368,6 @@ class DomainRequest(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="zip code",
|
verbose_name="zip code",
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
urbanization = models.CharField(
|
urbanization = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -938,3 +945,133 @@ class DomainRequest(TimeStampedModel):
|
||||||
for field in opts.many_to_many:
|
for field in opts.many_to_many:
|
||||||
data[field.name] = field.value_from_object(self)
|
data[field.name] = field.value_from_object(self)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def _is_federal_complete(self):
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def _is_interstate_complete(self):
|
||||||
|
# Interstate -> "About your organization" page can't be empty
|
||||||
|
return self.about_your_organization is not None
|
||||||
|
|
||||||
|
def _is_state_or_territory_complete(self):
|
||||||
|
# State -> ""Election office" page can't be empty
|
||||||
|
return self.is_election_board is not None
|
||||||
|
|
||||||
|
def _is_tribal_complete(self):
|
||||||
|
# Tribal -> "Tribal name" and "Election office" page can't be empty
|
||||||
|
return self.tribe_name is not None and self.is_election_board is not None
|
||||||
|
|
||||||
|
def _is_county_complete(self):
|
||||||
|
# County -> "Election office" page can't be empty
|
||||||
|
return self.is_election_board is not None
|
||||||
|
|
||||||
|
def _is_city_complete(self):
|
||||||
|
# City -> "Election office" page can't be empty
|
||||||
|
return self.is_election_board is not None
|
||||||
|
|
||||||
|
def _is_special_district_complete(self):
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def _is_organization_name_and_address_complete(self):
|
||||||
|
return not (
|
||||||
|
self.organization_name is None
|
||||||
|
and self.address_line1 is None
|
||||||
|
and self.city is None
|
||||||
|
and self.state_territory is None
|
||||||
|
and self.zipcode is None
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_authorizing_official_complete(self):
|
||||||
|
return self.authorizing_official is not None
|
||||||
|
|
||||||
|
def _is_requested_domain_complete(self):
|
||||||
|
return self.requested_domain is not None
|
||||||
|
|
||||||
|
def _is_purpose_complete(self):
|
||||||
|
return self.purpose is not None
|
||||||
|
|
||||||
|
def _is_submitter_complete(self):
|
||||||
|
return self.submitter is not None
|
||||||
|
|
||||||
|
def _has_other_contacts_and_filled(self):
|
||||||
|
# Other Contacts Radio button is Yes and if all required fields are filled
|
||||||
|
return (
|
||||||
|
self.has_other_contacts()
|
||||||
|
and self.other_contacts.filter(
|
||||||
|
first_name__isnull=False,
|
||||||
|
last_name__isnull=False,
|
||||||
|
title__isnull=False,
|
||||||
|
email__isnull=False,
|
||||||
|
phone__isnull=False,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _has_no_other_contacts_gives_rationale(self):
|
||||||
|
# Other Contacts Radio button is No and a rationale is provided
|
||||||
|
return self.has_other_contacts() is False and self.no_other_contacts_rationale is not None
|
||||||
|
|
||||||
|
def _is_other_contacts_complete(self):
|
||||||
|
if self._has_other_contacts_and_filled() or self._has_no_other_contacts_gives_rationale():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _cisa_rep_and_email_check(self):
|
||||||
|
# Has a CISA rep + email is NOT empty or NOT an empty string OR doesn't have CISA rep
|
||||||
|
return (
|
||||||
|
self.has_cisa_representative is True
|
||||||
|
and self.cisa_representative_email is not None
|
||||||
|
and self.cisa_representative_email != ""
|
||||||
|
) or self.has_cisa_representative is False
|
||||||
|
|
||||||
|
def _anything_else_radio_button_and_text_field_check(self):
|
||||||
|
# Anything else boolean is True + filled text field and it's not an empty string OR the boolean is No
|
||||||
|
return (
|
||||||
|
self.has_anything_else_text is True and self.anything_else is not None and self.anything_else != ""
|
||||||
|
) or self.has_anything_else_text is False
|
||||||
|
|
||||||
|
def _is_additional_details_complete(self):
|
||||||
|
return self._cisa_rep_and_email_check() and self._anything_else_radio_button_and_text_field_check()
|
||||||
|
|
||||||
|
def _is_policy_acknowledgement_complete(self):
|
||||||
|
return self.is_policy_acknowledged is not None
|
||||||
|
|
||||||
|
def _is_general_form_complete(self):
|
||||||
|
return (
|
||||||
|
self._is_organization_name_and_address_complete()
|
||||||
|
and self._is_authorizing_official_complete()
|
||||||
|
and self._is_requested_domain_complete()
|
||||||
|
and self._is_purpose_complete()
|
||||||
|
and self._is_submitter_complete()
|
||||||
|
and self._is_other_contacts_complete()
|
||||||
|
and self._is_additional_details_complete()
|
||||||
|
and self._is_policy_acknowledgement_complete()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _form_complete(self):
|
||||||
|
match self.generic_org_type:
|
||||||
|
case DomainRequest.OrganizationChoices.FEDERAL:
|
||||||
|
is_complete = self._is_federal_complete()
|
||||||
|
case DomainRequest.OrganizationChoices.INTERSTATE:
|
||||||
|
is_complete = self._is_interstate_complete()
|
||||||
|
case DomainRequest.OrganizationChoices.STATE_OR_TERRITORY:
|
||||||
|
is_complete = self._is_state_or_territory_complete()
|
||||||
|
case DomainRequest.OrganizationChoices.TRIBAL:
|
||||||
|
is_complete = self._is_tribal_complete()
|
||||||
|
case DomainRequest.OrganizationChoices.COUNTY:
|
||||||
|
is_complete = self._is_county_complete()
|
||||||
|
case DomainRequest.OrganizationChoices.CITY:
|
||||||
|
is_complete = self._is_city_complete()
|
||||||
|
case DomainRequest.OrganizationChoices.SPECIAL_DISTRICT:
|
||||||
|
is_complete = self._is_special_district_complete()
|
||||||
|
case DomainRequest.OrganizationChoices.SCHOOL_DISTRICT:
|
||||||
|
is_complete = True
|
||||||
|
case _:
|
||||||
|
# NOTE: Shouldn't happen, this is only if somehow they didn't choose an org type
|
||||||
|
is_complete = False
|
||||||
|
|
||||||
|
if not is_complete or not self._is_general_form_complete():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
@ -59,7 +59,6 @@ class TransitionDomain(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Organization name",
|
help_text="Organization name",
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
federal_type = models.CharField(
|
federal_type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
|
@ -85,7 +84,6 @@ class TransitionDomain(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="First name / given name",
|
help_text="First name / given name",
|
||||||
verbose_name="first name",
|
verbose_name="first name",
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
middle_name = models.CharField(
|
middle_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -136,7 +134,6 @@ class TransitionDomain(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="zip code",
|
verbose_name="zip code",
|
||||||
help_text="Zip code",
|
help_text="Zip code",
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -31,6 +31,17 @@ class User(AbstractUser):
|
||||||
will be updated if any updates are made to it through Login.gov.
|
will be updated if any updates are made to it through Login.gov.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["username"]),
|
||||||
|
models.Index(fields=["email"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
permissions = [
|
||||||
|
("analyst_access_permission", "Analyst Access Permission"),
|
||||||
|
("full_access_permission", "Full Access Permission"),
|
||||||
|
]
|
||||||
|
|
||||||
class VerificationTypeChoices(models.TextChoices):
|
class VerificationTypeChoices(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
Users achieve access to our system in a few different ways.
|
Users achieve access to our system in a few different ways.
|
||||||
|
@ -76,8 +87,6 @@ class User(AbstractUser):
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Phone",
|
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
middle_name = models.CharField(
|
middle_name = models.CharField(
|
||||||
|
@ -98,6 +107,24 @@ class User(AbstractUser):
|
||||||
help_text="The means through which this user was verified",
|
help_text="The means through which this user was verified",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def finished_setup(self):
|
||||||
|
"""
|
||||||
|
Tracks if the user finished their profile setup or not. This is so
|
||||||
|
we can globally enforce that new users provide additional account information before proceeding.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Change this to self once the user and contact objects are merged.
|
||||||
|
# For now, since they are linked, lets test on the underlying contact object.
|
||||||
|
user_info = self.contact # noqa
|
||||||
|
user_values = [
|
||||||
|
user_info.first_name,
|
||||||
|
user_info.last_name,
|
||||||
|
user_info.title,
|
||||||
|
user_info.phone,
|
||||||
|
]
|
||||||
|
return None not in user_values
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# this info is pulled from Login.gov
|
# this info is pulled from Login.gov
|
||||||
if self.first_name or self.last_name:
|
if self.first_name or self.last_name:
|
||||||
|
@ -263,9 +290,3 @@ class User(AbstractUser):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.check_domain_invitations_on_login()
|
self.check_domain_invitations_on_login()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
permissions = [
|
|
||||||
("analyst_access_permission", "Analyst Access Permission"),
|
|
||||||
("full_access_permission", "Full Access Permission"),
|
|
||||||
]
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlparse, urlunparse, urlencode
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -266,3 +267,34 @@ class CreateOrUpdateOrganizationTypeHelper:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def replace_url_queryparams(url_to_modify: str, query_params, convert_list_to_csv=False):
|
||||||
|
"""
|
||||||
|
Replaces the query parameters of a given URL.
|
||||||
|
Because this replaces them, this can be used to either add, delete, or modify.
|
||||||
|
Args:
|
||||||
|
url_to_modify (str): The URL whose query parameters need to be modified.
|
||||||
|
query_params (dict): Dictionary of query parameters to use.
|
||||||
|
convert_list_to_csv (bool): If the queryparam contains a list of items,
|
||||||
|
convert it to a csv representation instead.
|
||||||
|
Returns:
|
||||||
|
str: The modified URL with the updated query parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Ensure each key in query_params maps to a single value, not a list
|
||||||
|
if convert_list_to_csv:
|
||||||
|
for key, value in query_params.items():
|
||||||
|
if isinstance(value, list):
|
||||||
|
query_params[key] = ",".join(value)
|
||||||
|
|
||||||
|
# Split the URL into parts
|
||||||
|
url_parts = list(urlparse(url_to_modify))
|
||||||
|
|
||||||
|
# Modify the query param bit
|
||||||
|
url_parts[4] = urlencode(query_params)
|
||||||
|
|
||||||
|
# Reassemble the URL
|
||||||
|
new_url = urlunparse(url_parts)
|
||||||
|
|
||||||
|
return new_url
|
||||||
|
|
|
@ -9,7 +9,6 @@ class VerifiedByStaff(TimeStampedModel):
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
db_index=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
requestor = models.ForeignKey(
|
requestor = models.ForeignKey(
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
"""Middleware to add Cache-control: no-cache to every response.
|
|
||||||
|
|
||||||
Used to force Cloudfront caching to leave us alone while we develop
|
|
||||||
better caching responses.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class NoCacheMiddleware:
|
|
||||||
"""Middleware to add a single header to every response."""
|
|
||||||
|
|
||||||
def __init__(self, get_response):
|
|
||||||
self.get_response = get_response
|
|
||||||
|
|
||||||
def __call__(self, request):
|
|
||||||
response = self.get_response(request)
|
|
||||||
response["Cache-Control"] = "no-cache"
|
|
||||||
return response
|
|
100
src/registrar/registrar_middleware.py
Normal file
100
src/registrar/registrar_middleware.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
"""
|
||||||
|
Contains middleware used in settings.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
|
|
||||||
|
|
||||||
|
class NoCacheMiddleware:
|
||||||
|
"""
|
||||||
|
Middleware to add Cache-control: no-cache to every response.
|
||||||
|
|
||||||
|
Used to force Cloudfront caching to leave us alone while we develop
|
||||||
|
better caching responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
response["Cache-Control"] = "no-cache"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class CheckUserProfileMiddleware:
|
||||||
|
"""
|
||||||
|
Checks if the current user has finished_setup = False.
|
||||||
|
If they do, redirect them to the setup page regardless of where they are in
|
||||||
|
the application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
self.setup_page = reverse("finish-user-profile-setup")
|
||||||
|
self.logout_page = reverse("logout")
|
||||||
|
self.excluded_pages = [
|
||||||
|
self.setup_page,
|
||||||
|
self.logout_page,
|
||||||
|
]
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||||
|
"""Runs pre-processing logic for each view. Checks for the
|
||||||
|
finished_setup flag on the current user. If they haven't done so,
|
||||||
|
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 hasattr(request.user, "finished_setup") and not request.user.finished_setup:
|
||||||
|
return self._handle_setup_not_finished(request)
|
||||||
|
|
||||||
|
# Continue processing the view
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_setup_not_finished(self, request):
|
||||||
|
"""Redirects the given user to the finish setup page.
|
||||||
|
|
||||||
|
We set the "redirect" query param equal to where the user wants to go.
|
||||||
|
|
||||||
|
If the user wants to go to '/request/', then we set that
|
||||||
|
information in the query param.
|
||||||
|
|
||||||
|
Otherwise, we assume they want to go to the home page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# In some cases, we don't want to redirect to home. This handles that.
|
||||||
|
# Can easily be generalized if need be, but for now lets keep this easy to read.
|
||||||
|
custom_redirect = "domain-request:" if request.path == "/request/" else None
|
||||||
|
|
||||||
|
# Don't redirect on excluded pages (such as the setup page itself)
|
||||||
|
if not any(request.path.startswith(page) for page in self.excluded_pages):
|
||||||
|
|
||||||
|
# Preserve the original query parameters, and coerce them into a dict
|
||||||
|
query_params = parse_qs(request.META["QUERY_STRING"])
|
||||||
|
|
||||||
|
# Set the redirect value to our redirect location
|
||||||
|
if custom_redirect is not None:
|
||||||
|
query_params["redirect"] = custom_redirect
|
||||||
|
|
||||||
|
# Add our new query param, while preserving old ones
|
||||||
|
new_setup_page = replace_url_queryparams(self.setup_page, query_params) if query_params else self.setup_page
|
||||||
|
|
||||||
|
return HttpResponseRedirect(new_setup_page)
|
||||||
|
else:
|
||||||
|
# Process the view as normal
|
||||||
|
return None
|
|
@ -24,9 +24,11 @@ def handle_profile(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
first_name = getattr(instance, "first_name", "")
|
first_name = getattr(instance, "first_name", "")
|
||||||
|
middle_name = getattr(instance, "middle_name", "")
|
||||||
last_name = getattr(instance, "last_name", "")
|
last_name = getattr(instance, "last_name", "")
|
||||||
email = getattr(instance, "email", "")
|
email = getattr(instance, "email", "")
|
||||||
phone = getattr(instance, "phone", "")
|
phone = getattr(instance, "phone", "")
|
||||||
|
title = getattr(instance, "title", "")
|
||||||
|
|
||||||
is_new_user = kwargs.get("created", False)
|
is_new_user = kwargs.get("created", False)
|
||||||
|
|
||||||
|
@ -39,9 +41,11 @@ def handle_profile(sender, instance, **kwargs):
|
||||||
Contact.objects.create(
|
Contact.objects.create(
|
||||||
user=instance,
|
user=instance,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
|
middle_name=middle_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
email=email,
|
email=email,
|
||||||
phone=phone,
|
phone=phone,
|
||||||
|
title=title,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(contacts) >= 1 and is_new_user: # a matching contact
|
if len(contacts) >= 1 and is_new_user: # a matching contact
|
||||||
|
|
|
@ -26,10 +26,21 @@
|
||||||
<script type="application/javascript" src="{% static 'js/dja-collapse.js' %}" defer></script>
|
<script type="application/javascript" src="{% static 'js/dja-collapse.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
{% block title %}
|
||||||
|
{% if subtitle %}
|
||||||
|
{{ subtitle }} |
|
||||||
|
{% endif %}
|
||||||
|
{% if tabtitle %}
|
||||||
|
{{ tabtitle }} |
|
||||||
|
{% else %}
|
||||||
|
{{ title }} |
|
||||||
|
{% endif %}
|
||||||
|
{{ site_title|default:_('Django site admin') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrastyle %}{{ block.super }}
|
{% block extrastyle %}{{ block.super }}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
|
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if not IS_PRODUCTION %}
|
{% if not IS_PRODUCTION %}
|
||||||
|
|
|
@ -45,6 +45,8 @@
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
|
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
|
||||||
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
||||||
|
<!-- We override with our own copy to make some classes accessible in our JS -->
|
||||||
|
<script src="{% static 'js/uswds-edited.js' %}" defer></script>
|
||||||
<script src="{% static 'js/get-gov.js' %}" defer></script>
|
<script src="{% static 'js/get-gov.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -67,7 +69,6 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="{% block body_id %}default{% endblock %}" class="{% block body_class %}section_front{% endblock %}">
|
<body id="{% block body_id %}default{% endblock %}" class="{% block body_class %}section_front{% endblock %}">
|
||||||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
|
||||||
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
||||||
|
|
||||||
{% if not IS_PRODUCTION %}
|
{% if not IS_PRODUCTION %}
|
||||||
|
@ -138,11 +139,7 @@
|
||||||
<div class="usa-nav-container">
|
<div class="usa-nav-container">
|
||||||
<div class="usa-navbar">
|
<div class="usa-navbar">
|
||||||
{% block logo %}
|
{% block logo %}
|
||||||
<div class="usa-logo display-inline-block" id="extended-logo">
|
{% include "includes/gov_extended_logo.html" with logo_clickable=True %}
|
||||||
<strong class="usa-logo__text" >
|
|
||||||
<a href="{% url 'home' %}">.gov Registrar </a>
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<button type="button" class="usa-menu-btn">Menu</button>
|
<button type="button" class="usa-menu-btn">Menu</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -159,7 +156,8 @@
|
||||||
{% if has_profile_feature_flag %}
|
{% 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 %}
|
||||||
<a class="usa-nav-link {% if request.path == user_profile_url %}usa-current{% endif %}" href="{{ user_profile_url }}">
|
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
||||||
|
<a class="usa-nav-link {% if request.path == user_profile_url or request.path == finish_setup_url %}usa-current{% endif %}" href="{{ user_profile_url }}">
|
||||||
<span class="text-primary">Your profile</span>
|
<span class="text-primary">Your profile</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -205,7 +203,9 @@
|
||||||
</div>
|
</div>
|
||||||
{% endblock wrapper%}
|
{% endblock wrapper%}
|
||||||
|
|
||||||
{% include "includes/footer.html" %}
|
{% block footer %}
|
||||||
|
{% include "includes/footer.html" with show_manage_your_domains=True %}
|
||||||
|
{% endblock footer %}
|
||||||
</div> <!-- /#wrapper -->
|
</div> <!-- /#wrapper -->
|
||||||
|
|
||||||
{% block init_js %}{% endblock %}{# useful for vars and other initializations #}
|
{% block init_js %}{% endblock %}{# useful for vars and other initializations #}
|
||||||
|
|
36
src/registrar/templates/django/admin/user_change_form.html
Normal file
36
src/registrar/templates/django/admin/user_change_form.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends 'django/admin/email_clipboard_change_form.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block after_related_objects %}
|
||||||
|
<div class="module aligned padding-3">
|
||||||
|
<h2>Associated requests and domains</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>Domain requests</h3>
|
||||||
|
<ul class="margin-0 padding-0">
|
||||||
|
{% for domain_request in domain_requests %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:registrar_domainrequest_change' domain_request.pk %}">
|
||||||
|
{{ domain_request.requested_domain }}
|
||||||
|
</a>
|
||||||
|
({{ domain_request.status }})
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||||
|
<h3>Domains</h3>
|
||||||
|
<ul class="margin-0 padding-0">
|
||||||
|
{% for domain in domains %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:registrar_domain_change' domain.pk %}">
|
||||||
|
{{ domain.name }}
|
||||||
|
</a>
|
||||||
|
({{ domain.state }})
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "domain_base.html" %}
|
{% extends "domain_base.html" %}
|
||||||
{% load static field_helpers url_helpers %}
|
{% load static field_helpers url_helpers %}
|
||||||
|
|
||||||
{% block title %}Domain authorizing official | {{ domain.name }} | {% endblock %}
|
{% block title %}Authorizing official | {{ domain.name }} | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
{# this is right after the messages block in the parent template #}
|
{# this is right after the messages block in the parent template #}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Domain: {{ domain.name }} | {% endblock %}
|
{% block title %}{{ domain.name }} | {% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
aria-describedby="Are you sure you want to submit a domain request?"
|
aria-describedby="Are you sure you want to submit a domain request?"
|
||||||
data-force-action
|
data-force-action
|
||||||
>
|
>
|
||||||
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you won’t be able to edit it until we review it. You’ll only be able to withdraw your request." modal_button=modal_button|safe %}
|
{% include 'includes/modal.html' with is_domain_request_form=True review_form_is_complete=review_form_is_complete modal_heading=modal_heading|safe modal_description=modal_description|safe modal_button=modal_button|safe %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block after_form_content %}{% endblock %}
|
{% block after_form_content %}{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static form_helpers url_helpers %}
|
{% load static form_helpers url_helpers %}
|
||||||
|
|
||||||
|
{% block title %} Start a request | {% endblock %}
|
||||||
|
|
||||||
{% 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">
|
||||||
|
@ -13,12 +15,12 @@
|
||||||
<p>We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.</p>
|
<p>We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.</p>
|
||||||
<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 %}
|
{% 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="" target="_blank" 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' %}?return_to_request=True" 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 %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% block form_buttons %}
|
{% block form_buttons %}
|
||||||
|
|
|
@ -25,11 +25,11 @@
|
||||||
{% if step == Step.ORGANIZATION_TYPE %}
|
{% if step == Step.ORGANIZATION_TYPE %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% if domain_request.generic_org_type is not None %}
|
{% if domain_request.generic_org_type is not None %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% if domain_request.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
{% if domain_request.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
|
|
||||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url address='true' %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url address='true' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with title=form_titles|get_item:step value='Incomplete' %}
|
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
|
|
||||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
{% 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 %}
|
||||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
|
|
||||||
{% if step == Step.DOTGOV_DOMAIN %}
|
{% if step == Step.DOTGOV_DOMAIN %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe%}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@
|
||||||
|
|
||||||
{% if step == Step.PURPOSE %}
|
{% if step == Step.PURPOSE %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
{% 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 %}
|
||||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -148,7 +148,7 @@
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' list='true' %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' list='true' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -157,7 +157,7 @@
|
||||||
|
|
||||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
|
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
|
20
src/registrar/templates/finish_profile_setup.html
Normal file
20
src/registrar/templates/finish_profile_setup.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "profile.html" %}
|
||||||
|
|
||||||
|
{% load static form_helpers url_helpers field_helpers %}
|
||||||
|
{% block title %} Finish setting up your profile | {% endblock %}
|
||||||
|
|
||||||
|
{# Disable the redirect #}
|
||||||
|
{% block logo %}
|
||||||
|
{% include "includes/gov_extended_logo.html" with logo_clickable=confirm_changes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{# Add the new form #}
|
||||||
|
{% block content_bottom %}
|
||||||
|
{% include "includes/finish_profile_form.html" with form=form %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock content_bottom %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{% include "includes/footer.html" with show_manage_your_domains=confirm_changes %}
|
||||||
|
{% endblock footer %}
|
|
@ -24,224 +24,123 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section class="section--outlined">
|
<section class="section--outlined">
|
||||||
<h2>Domains</h2>
|
<h2 id="domains-header">Domains</h2>
|
||||||
{% if domains %}
|
<div class="domains-wrapper display-none">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__registered-domains">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__registered-domains">
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable scope="col" role="columnheader">Expires</th>
|
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
||||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
>
|
|
||||||
<span class="usa-sr-only">Action</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for domain in domains %}
|
|
||||||
<tr>
|
|
||||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
|
||||||
{{ domain.name }}
|
|
||||||
</th>
|
|
||||||
<td data-sort-value="{{ domain.expiration_date|date:"U" }}" data-label="Expires">{{ domain.expiration_date|date }}</td>
|
|
||||||
<td data-label="Status">
|
|
||||||
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
|
||||||
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
|
||||||
Expired
|
|
||||||
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
|
||||||
DNS needed
|
|
||||||
{% else %}
|
|
||||||
{{ domain.state|capfirst }}
|
|
||||||
{% endif %}
|
|
||||||
<svg
|
|
||||||
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
|
|
||||||
data-position="top"
|
|
||||||
title="{{domain.get_state_help_text}}"
|
|
||||||
focusable="true"
|
|
||||||
aria-label="Status Information"
|
|
||||||
role="tooltip"
|
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#info_outline"></use>
|
<span class="usa-sr-only">Action</span>
|
||||||
</svg>
|
</th>
|
||||||
</td>
|
</tr>
|
||||||
<td>
|
</thead>
|
||||||
<a href="{% url "domain" pk=domain.pk %}">
|
<tbody>
|
||||||
<svg
|
<!-- AJAX will populate this tbody -->
|
||||||
class="usa-icon"
|
</tbody>
|
||||||
aria-hidden="true"
|
</table>
|
||||||
focusable="false"
|
<div
|
||||||
role="img"
|
class="usa-sr-only usa-table__announcement-region"
|
||||||
width="24"
|
aria-live="polite"
|
||||||
>
|
></div>
|
||||||
{% if domain.state == "deleted" or domain.state == "on hold" %}
|
</div>
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#visibility"></use>
|
<div class="no-domains-wrapper display-none">
|
||||||
</svg>
|
<p>You don't have any registered domains.</p>
|
||||||
View <span class="usa-sr-only">{{ domain.name }}</span>
|
<p class="maxw-none clearfix">
|
||||||
{% else %}
|
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
</svg>
|
<use xlink:href="{%static 'img/sprite.svg'%}#help_outline"></use>
|
||||||
Manage <span class="usa-sr-only">{{ domain.name }}</span>
|
</svg>
|
||||||
{% endif %}
|
Why don't I see my domain when I sign in to the registrar?
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</p>
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div
|
|
||||||
class="usa-sr-only usa-table__announcement-region"
|
|
||||||
aria-live="polite"
|
|
||||||
></div>
|
|
||||||
{% else %}
|
|
||||||
<p>You don't have any registered domains.</p>
|
|
||||||
<p class="maxw-none clearfix">
|
|
||||||
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#help_outline"></use>
|
|
||||||
</svg>
|
|
||||||
Why don't I see my domain when I sign in to the registrar?
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
</section>
|
||||||
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
||||||
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
|
<!-- Count will be dynamically populated by JS -->
|
||||||
|
</span>
|
||||||
|
<ul class="usa-pagination__list">
|
||||||
|
<!-- Pagination links will be dynamically populated by JS -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<section class="section--outlined">
|
<section class="section--outlined">
|
||||||
<h2>Domain requests</h2>
|
<h2 id="domain-requests-header">Domain requests</h2>
|
||||||
{% if domain_requests %}
|
<div class="domain-requests-wrapper display-none">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__domain-requests">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table__domain-requests">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable scope="col" role="columnheader">Date submitted</th>
|
<th data-sortable="submission_date" scope="col" role="columnheader">Date submitted</th>
|
||||||
<th data-sortable 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>
|
||||||
{% if has_deletable_domain_requests %}
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Delete Action</span></th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Delete Action</span></th>
|
</tr>
|
||||||
{% endif %}
|
</thead>
|
||||||
</tr>
|
<tbody id="domain-requests-tbody">
|
||||||
</thead>
|
<!-- AJAX will populate this tbody -->
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for domain_request in domain_requests %}
|
</table>
|
||||||
<tr>
|
<div
|
||||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
class="usa-sr-only usa-table__announcement-region"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{% for domain_request in domain_requests %}
|
||||||
|
{% if has_deletable_domain_requests %}
|
||||||
|
{% if domain_request.status == domain_request.DomainRequestStatus.STARTED or domain_request.status == domain_request.DomainRequestStatus.WITHDRAWN %}
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="toggle-delete-domain-alert-{{ domain_request.id }}"
|
||||||
|
aria-labelledby="Are you sure you want to continue?"
|
||||||
|
aria-describedby="Domain will be removed"
|
||||||
|
data-force-action
|
||||||
|
>
|
||||||
|
<form method="POST" action="{% url "domain-request-delete" pk=domain_request.id %}">
|
||||||
{% if domain_request.requested_domain is None %}
|
{% if domain_request.requested_domain is None %}
|
||||||
New domain request
|
{% if domain_request.created_at %}
|
||||||
{# Add a breakpoint #}
|
{% with prefix="(created " %}
|
||||||
<div aria-hidden="true"></div>
|
{% with formatted_date=domain_request.created_at|date:"DATETIME_FORMAT" %}
|
||||||
<span class="text-base font-body-xs">({{ domain_request.created_at }} UTC)</span>
|
{% with modal_content=prefix|add:formatted_date|add:" UTC)" %}
|
||||||
{% else %}
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this domain request?" modal_description="This will remove the domain request "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||||
{{ domain_request.requested_domain.name }}
|
{% endwith %}
|
||||||
{% endif %}
|
|
||||||
</th>
|
|
||||||
<td data-sort-value="{{ domain_request.submission_date|date:"U" }}" data-label="Date submitted">
|
|
||||||
{% if domain_request.submission_date %}
|
|
||||||
{{ domain_request.submission_date|date }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-base">Not submitted</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td data-label="Status">{{ domain_request.get_status_display }}</td>
|
|
||||||
<td>
|
|
||||||
{% with prefix="New domain request ("%}
|
|
||||||
{% with date=domain_request.created_at|date:"DATETIME_FORMAT"%}
|
|
||||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
|
||||||
{% if domain_request.status == domain_request.DomainRequestStatus.STARTED or domain_request.status == domain_request.DomainRequestStatus.ACTION_NEEDED or domain_request.status == domain_request.DomainRequestStatus.WITHDRAWN %}
|
|
||||||
<a href="{% url 'edit-domain-request' domain_request.pk %}">
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
|
||||||
</svg>
|
|
||||||
{% if domain_request.requested_domain is not None%}
|
|
||||||
Edit <span class="usa-sr-only">{{ domain_request.requested_domain.name }}</span>
|
|
||||||
{% else %}
|
|
||||||
Edit <span class="usa-sr-only">{{ name_default }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'domain-request-status' domain_request.pk %}">
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
|
||||||
</svg>
|
|
||||||
Manage <span class="usa-sr-only">{{ domain_request.requested_domain.name|default:name_default }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
{% if has_deletable_domain_requests %}
|
|
||||||
<td>
|
|
||||||
{% if domain_request.status == "started" or domain_request.status == "withdrawn" %}
|
|
||||||
<a
|
|
||||||
role="button"
|
|
||||||
id="button-toggle-delete-domain-alert-{{ forloop.counter }}"
|
|
||||||
href="#toggle-delete-domain-alert-{{ forloop.counter }}"
|
|
||||||
class="usa-button--unstyled text-no-underline"
|
|
||||||
aria-controls="toggle-delete-domain-alert-{{ forloop.counter }}"
|
|
||||||
data-open-modal
|
|
||||||
>
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
|
||||||
</svg>
|
|
||||||
{% with prefix="New domain request ("%}
|
|
||||||
{% with date=domain_request.created_at|date:"DATETIME_FORMAT"%}
|
|
||||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
|
||||||
{% if domain_request.requested_domain is not None %}
|
|
||||||
Delete <span class="usa-sr-only">{{ domain_request.requested_domain.name }}</span>
|
|
||||||
{% else %}
|
|
||||||
Delete <span class="usa-sr-only">{{ name_default }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% with modal_heading_value=domain_request.requested_domain.name|add:"?" %}
|
||||||
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value=modal_heading_value modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</a>
|
{% endif %}
|
||||||
|
</form>
|
||||||
<div
|
</div>
|
||||||
class="usa-modal"
|
|
||||||
id="toggle-delete-domain-alert-{{ forloop.counter }}"
|
|
||||||
aria-labelledby="Are you sure you want to continue?"
|
|
||||||
aria-describedby="Domain will be removed"
|
|
||||||
data-force-action
|
|
||||||
>
|
|
||||||
<form method="POST" action="{% url "domain-request-delete" pk=domain_request.id %}">
|
|
||||||
{% if domain_request.requested_domain is None %}
|
|
||||||
{% if domain_request.created_at %}
|
|
||||||
{% with prefix="(created " %}
|
|
||||||
{% with formatted_date=domain_request.created_at|date:"DATETIME_FORMAT" %}
|
|
||||||
{% with modal_content=prefix|add:formatted_date|add:" UTC)" %}
|
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this domain request?" modal_description="This will remove the domain request "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% else %}
|
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% with modal_heading_value=domain_request.requested_domain.name|add:"?" %}
|
|
||||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value=modal_heading_value modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
|
||||||
</table>
|
</div>
|
||||||
<div
|
<div class="no-domain-requests-wrapper display-none">
|
||||||
class="usa-sr-only usa-table__announcement-region"
|
<p>You haven't requested any domains.</p>
|
||||||
aria-live="polite"
|
</div>
|
||||||
></div>
|
|
||||||
{% else %}
|
|
||||||
<p>You haven't requested any domains.</p>
|
|
||||||
<!-- <p><a href="{% url 'domain-request:' %}" class="usa-button">Start a new domain request</a></p> -->
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
</section>
|
||||||
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
||||||
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
|
<!-- Count will be dynamically populated by JS -->
|
||||||
|
</span>
|
||||||
|
<ul class="usa-pagination__list">
|
||||||
|
<!-- Pagination links will be dynamically populated by JS -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
{# Note: Reimplement this after MVP #}
|
{# Note: Reimplement this after MVP #}
|
||||||
<!--
|
<!--
|
||||||
|
|
89
src/registrar/templates/includes/finish_profile_form.html
Normal file
89
src/registrar/templates/includes/finish_profile_form.html
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
{% extends 'includes/profile_form.html' %}
|
||||||
|
|
||||||
|
{% load static url_helpers %}
|
||||||
|
{% load field_helpers %}
|
||||||
|
|
||||||
|
{% block profile_header %}
|
||||||
|
<h1>Finish setting up your profile</h1>
|
||||||
|
{% endblock profile_header %}
|
||||||
|
|
||||||
|
{% block profile_blurb %}
|
||||||
|
<p>
|
||||||
|
We <a class="usa-link usa-link--always-blue" href="{% public_site_url 'domains/requirements/#keep-your-contact-information-updated' %}" target="_blank">require</a>
|
||||||
|
that you maintain accurate contact information.
|
||||||
|
The details you provide will only be used to support the administration of .gov and won’t be made public.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>What contact information should we use to reach you?</h2>
|
||||||
|
<p>
|
||||||
|
Review the details below and update any required information.
|
||||||
|
Note that editing this information won’t affect your Login.gov account information.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{# We use a var called 'remove_margin_top' rather than 'add_margin_top' because this is more useful as a default #}
|
||||||
|
{% include "includes/required_fields.html" with remove_margin_top=True %}
|
||||||
|
|
||||||
|
{% endblock profile_blurb %}
|
||||||
|
|
||||||
|
{% block profile_form %}
|
||||||
|
<form id="finish-profile-setup-form" class="usa-form usa-form--largest" method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset class="usa-fieldset">
|
||||||
|
<legend class="usa-sr-only">
|
||||||
|
Your contact information
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
{% with show_edit_button=True show_readonly=True group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %}
|
||||||
|
{% input_with_errors form.full_name %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div id="profile-name-group" class="display-none" role="group">
|
||||||
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %}
|
||||||
|
{% input_with_errors form.first_name %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with group_classes="usa-form-editable padding-top-2" %}
|
||||||
|
{% input_with_errors form.middle_name %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with group_classes="usa-form-editable padding-top-2" %}
|
||||||
|
{% input_with_errors form.last_name %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
||||||
|
{% with show_readonly=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %}
|
||||||
|
{% with link_href=login_help_url %}
|
||||||
|
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
||||||
|
{% with link_text="Get help with your Login.gov account" target_blank=True do_not_show_max_chars=True %}
|
||||||
|
{% input_with_errors form.email %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %}
|
||||||
|
{% input_with_errors form.title %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %}
|
||||||
|
{% with add_class="usa-input--medium" %}
|
||||||
|
{% input_with_errors form.phone %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<button type="submit" name="contact_setup_save_button" class="usa-button ">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{% if confirm_changes and going_to_specific_page %}
|
||||||
|
<button type="submit" name="contact_setup_submit_button" class="usa-button usa-button--outline">
|
||||||
|
{{redirect_button_text }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock profile_form %}
|
|
@ -26,10 +26,12 @@
|
||||||
>
|
>
|
||||||
<address class="usa-footer__address">
|
<address class="usa-footer__address">
|
||||||
<div class="usa-footer__contact-info grid-row grid-gap-md">
|
<div class="usa-footer__contact-info grid-row grid-gap-md">
|
||||||
|
{% if show_manage_your_domains %}
|
||||||
<div class="grid-col-auto">
|
<div class="grid-col-auto">
|
||||||
<a class="usa-link" rel="noopener noreferrer" href="{% url 'home' %}">Manage your domains</a>
|
<a class="usa-link" rel="noopener noreferrer" href="{% url 'home' %}">Manage your domains</a>
|
||||||
</div>
|
</div>
|
||||||
<span class=""> | </span>
|
<span class=""> | </span>
|
||||||
|
{% endif %}
|
||||||
<div class="grid-col-auto">
|
<div class="grid-col-auto">
|
||||||
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'help/' %}">Help </a>
|
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'help/' %}">Help </a>
|
||||||
</div>
|
</div>
|
||||||
|
|
17
src/registrar/templates/includes/gov_extended_logo.html
Normal file
17
src/registrar/templates/includes/gov_extended_logo.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{# Q: For reviewers -- What should this file be called? #}
|
||||||
|
|
||||||
|
<div class="usa-logo display-inline-block" id="extended-logo">
|
||||||
|
<strong class="usa-logo__text" >
|
||||||
|
{% if logo_clickable %}
|
||||||
|
<a href="{% url 'home' %}">.gov Registrar</a>
|
||||||
|
{% else %}
|
||||||
|
<button
|
||||||
|
class="usa-button--unstyled disabled-button usa-tooltip"
|
||||||
|
data-position="bottom"
|
||||||
|
title="Before you can manage your domains, we need you to add contact information."
|
||||||
|
data-tooltip="true"
|
||||||
|
role="button"
|
||||||
|
>.gov Registrar</button>
|
||||||
|
{% endif %}
|
||||||
|
</strong>
|
||||||
|
</div>
|
|
@ -2,7 +2,7 @@
|
||||||
Template include for form fields with classes and their corresponding
|
Template include for form fields with classes and their corresponding
|
||||||
error messages, if necessary.
|
error messages, if necessary.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
{% load static field_helpers url_helpers %}
|
||||||
{% load custom_filters %}
|
{% load custom_filters %}
|
||||||
|
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
@ -27,7 +27,11 @@ error messages, if necessary.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not field.widget_type == "checkbox" %}
|
{% if not field.widget_type == "checkbox" %}
|
||||||
{% include "django/forms/label.html" %}
|
{% if show_edit_button %}
|
||||||
|
{% include "includes/label_with_edit_button.html" with bold_label=True %}
|
||||||
|
{% else %}
|
||||||
|
{% include "django/forms/label.html" %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if sublabel_text %}
|
{% if sublabel_text %}
|
||||||
|
@ -58,6 +62,11 @@ error messages, if necessary.
|
||||||
{% if append_gov %}
|
{% if append_gov %}
|
||||||
<div class="display-flex flex-align-center">
|
<div class="display-flex flex-align-center">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_readonly %}
|
||||||
|
{% include "includes/readonly_input.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# this is the input field, itself #}
|
{# this is the input field, itself #}
|
||||||
{% include widget.template_name %}
|
{% include widget.template_name %}
|
||||||
|
|
||||||
|
|
14
src/registrar/templates/includes/label_with_edit_button.html
Normal file
14
src/registrar/templates/includes/label_with_edit_button.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
{% load static field_helpers url_helpers %}
|
||||||
|
<div class="grid-row {% if bold_label %}bold-usa-label{% endif %}">
|
||||||
|
<div class="grid-col">
|
||||||
|
{% include "django/forms/label.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="grid-col-2 text-right">
|
||||||
|
<button type="button" id="{{field.name}}__edit-button" class="usa-button usa-button--unstyled readonly-edit-button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||||
|
</svg>Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -18,12 +18,18 @@
|
||||||
|
|
||||||
<div class="usa-modal__footer">
|
<div class="usa-modal__footer">
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group">
|
||||||
|
{% if not_form %}
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ modal_button }}
|
{{ modal_button }}
|
||||||
</form>
|
</li>
|
||||||
</li>
|
{% else %}
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ modal_button }}
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||||
in addition to being a close modal hook {% endcomment %}
|
in addition to being a close modal hook {% endcomment %}
|
||||||
|
@ -39,7 +45,7 @@
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% elif not is_domain_request_form or review_form_is_complete %}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
|
50
src/registrar/templates/includes/profile_form.html
Normal file
50
src/registrar/templates/includes/profile_form.html
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{% load static url_helpers %}
|
||||||
|
{% load field_helpers %}
|
||||||
|
|
||||||
|
{% block profile_header %}
|
||||||
|
<h1>Your profile</h1>
|
||||||
|
{% endblock profile_header %}
|
||||||
|
|
||||||
|
{% block profile_blurb %}
|
||||||
|
<p>We <a class="usa-link usa-link--always-blue" href="{% public_site_url 'domains/requirements/#keep-your-contact-information-updated' %}" target="_blank">require</a> that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.</p>
|
||||||
|
|
||||||
|
<h2>Contact information</h2>
|
||||||
|
<p>Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.</p>
|
||||||
|
{% include "includes/required_fields.html" %}
|
||||||
|
{% endblock profile_blurb %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block profile_form %}
|
||||||
|
|
||||||
|
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
||||||
|
|
||||||
|
{% with link_href=login_help_url %}
|
||||||
|
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
||||||
|
{% with link_text="Get help with your Login.gov account" %}
|
||||||
|
{% with target_blank=True %}
|
||||||
|
{% with do_not_show_max_chars=True %}
|
||||||
|
{% input_with_errors form.email %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with add_class="usa-input--medium" %}
|
||||||
|
{% input_with_errors form.phone %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<button type="submit" class="usa-button">Save</button>
|
||||||
|
</form>
|
||||||
|
{% endblock profile_form %}
|
18
src/registrar/templates/includes/readonly_input.html
Normal file
18
src/registrar/templates/includes/readonly_input.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% load static field_helpers url_helpers custom_filters %}
|
||||||
|
|
||||||
|
<div id="{{field.name}}__edit-button-readonly" class="margin-top-2 margin-bottom-1 input-with-edit-button {% if not field.value and field.field.required %}input-with-edit-button__error{% endif %}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
{% if field.value or not field.field.required %}
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use>
|
||||||
|
{%elif not field.value %}
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
|
||||||
|
{%endif %}
|
||||||
|
</svg>
|
||||||
|
<div class="display-inline padding-left-05 margin-left-3 readonly-field {% if not field.field.required %}text-base{% endif %}">
|
||||||
|
{% if field.name != "phone" %}
|
||||||
|
{{ field.value }}
|
||||||
|
{% else %}
|
||||||
|
{{ field.value|format_phone }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,3 +1,3 @@
|
||||||
<p class="margin-top-3">
|
<p class="{% if not remove_margin_top %}margin-top-3 {% endif %}">
|
||||||
<em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
<em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,25 +1,13 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Edit your User Profile |
|
Edit your User Profile |
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% load static url_helpers %}
|
{% load static url_helpers %}
|
||||||
{% load field_helpers %}
|
|
||||||
|
|
||||||
{% 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">
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
|
||||||
Back to manage your domains
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
{# messages block is under the back breadcrumb link #}
|
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
|
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
|
||||||
|
@ -31,45 +19,27 @@ Edit your User Profile |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
<h1>Your profile</h1>
|
{% if show_back_button %}
|
||||||
<p>We <a href="{% public_site_url 'domains/requirements/#what-.gov-domain-registrants-must-do' %}" target="_blank">require</a> that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.</p>
|
<a href="{% if not return_to_request %}{% url 'home' %}{% else %}{% url 'domain-request:' %}{% endif %}" class="breadcrumb__back">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
<h2>Contact information</h2>
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
<p>Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.</p>
|
</svg>
|
||||||
|
{% if not return_to_request %}
|
||||||
|
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||||
|
{{ profile_back_button_text }}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||||
|
Go back to your domain request
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
|
||||||
|
|
||||||
{% with link_href=login_help_url %}
|
|
||||||
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
|
||||||
{% with link_text="Get help with your Login.gov account" %}
|
|
||||||
{% with target_blank=True %}
|
|
||||||
{% with do_not_show_max_chars=True %}
|
|
||||||
{% input_with_errors form.email %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% with add_class="usa-input--medium" %}
|
|
||||||
{% input_with_errors form.phone %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<button type="submit" class="usa-button">Save</button>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block content_bottom %}
|
||||||
|
{% include "includes/profile_form.html" with form=form %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock content_bottom %}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
from django import template
|
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
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -133,3 +134,14 @@ def get_region(state):
|
||||||
return regions.get(state.upper(), "N/A")
|
return regions.get(state.upper(), "N/A")
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def format_phone(value):
|
||||||
|
"""Converts a phonenumber to a national format"""
|
||||||
|
if value:
|
||||||
|
phone_number = value
|
||||||
|
if isinstance(value, str):
|
||||||
|
phone_number = PhoneNumber.from_string(value)
|
||||||
|
return phone_number.as_national
|
||||||
|
return value
|
||||||
|
|
|
@ -26,6 +26,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
||||||
add_group_class: append to input element's surrounding tag's `class` attribute
|
add_group_class: append to input element's surrounding tag's `class` attribute
|
||||||
attr_* - adds or replaces any single html attribute for the input
|
attr_* - adds or replaces any single html attribute for the input
|
||||||
add_error_attr_* - like `attr_*` but only if field.errors is not empty
|
add_error_attr_* - like `attr_*` but only if field.errors is not empty
|
||||||
|
show_edit_button: shows a simple edit button, and adds display-none to the input field.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
```
|
```
|
||||||
|
@ -91,6 +92,12 @@ def input_with_errors(context, field=None): # noqa: C901
|
||||||
elif key == "add_group_class":
|
elif key == "add_group_class":
|
||||||
group_classes.append(value)
|
group_classes.append(value)
|
||||||
|
|
||||||
|
elif key == "show_edit_button":
|
||||||
|
# Hide the primary input field.
|
||||||
|
# Used such that we can toggle it with JS
|
||||||
|
if "display-none" not in classes:
|
||||||
|
classes.append("display-none")
|
||||||
|
|
||||||
attrs["id"] = field.auto_id
|
attrs["id"] = field.auto_id
|
||||||
|
|
||||||
# do some work for various edge cases
|
# do some work for various edge cases
|
||||||
|
|
|
@ -667,7 +667,7 @@ class MockDb(TestCase):
|
||||||
is_election_board=False,
|
is_election_board=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
meoward_user = get_user_model().objects.create(
|
self.meoward_user = get_user_model().objects.create(
|
||||||
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
|
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -676,7 +676,7 @@ class MockDb(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
user=self.meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
@ -688,19 +688,21 @@ class MockDb(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
|
user=self.meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
user=meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER
|
user=self.meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
user=meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER
|
user=self.meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
_, created = DomainInvitation.objects.get_or_create(
|
_, created = DomainInvitation.objects.get_or_create(
|
||||||
email=meoward_user.email, domain=self.domain_1, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
|
email=self.meoward_user.email,
|
||||||
|
domain=self.domain_1,
|
||||||
|
status=DomainInvitation.DomainInvitationStatus.RETRIEVED,
|
||||||
)
|
)
|
||||||
|
|
||||||
_, created = DomainInvitation.objects.get_or_create(
|
_, created = DomainInvitation.objects.get_or_create(
|
||||||
|
|
|
@ -47,6 +47,7 @@ from registrar.models import (
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||||
from .common import (
|
from .common import (
|
||||||
|
MockDb,
|
||||||
MockSESClient,
|
MockSESClient,
|
||||||
AuditedAdminMockData,
|
AuditedAdminMockData,
|
||||||
completed_domain_request,
|
completed_domain_request,
|
||||||
|
@ -3438,16 +3439,19 @@ class TestListHeaderAdmin(TestCase):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
class TestMyUserAdmin(TestCase):
|
class TestMyUserAdmin(MockDb):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
admin_site = AdminSite()
|
admin_site = AdminSite()
|
||||||
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
self.staffuser = create_user()
|
||||||
self.test_helper = GenericTestHelper(admin=self.admin)
|
self.test_helper = GenericTestHelper(admin=self.admin)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -3472,7 +3476,7 @@ class TestMyUserAdmin(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests for the correct helper text on this page
|
Tests for the correct helper text on this page
|
||||||
"""
|
"""
|
||||||
user = create_user()
|
user = self.staffuser
|
||||||
|
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
@ -3493,10 +3497,11 @@ class TestMyUserAdmin(TestCase):
|
||||||
]
|
]
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_list_display_without_username(self):
|
def test_list_display_without_username(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
request = self.client.request().wsgi_request
|
request = self.client.request().wsgi_request
|
||||||
request.user = create_user()
|
request.user = self.staffuser
|
||||||
|
|
||||||
list_display = self.admin.get_list_display(request)
|
list_display = self.admin.get_list_display(request)
|
||||||
expected_list_display = [
|
expected_list_display = [
|
||||||
|
@ -3522,7 +3527,7 @@ class TestMyUserAdmin(TestCase):
|
||||||
def test_get_fieldsets_cisa_analyst(self):
|
def test_get_fieldsets_cisa_analyst(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
request = self.client.request().wsgi_request
|
request = self.client.request().wsgi_request
|
||||||
request.user = create_user()
|
request.user = self.staffuser
|
||||||
fieldsets = self.admin.get_fieldsets(request)
|
fieldsets = self.admin.get_fieldsets(request)
|
||||||
expected_fieldsets = (
|
expected_fieldsets = (
|
||||||
(
|
(
|
||||||
|
@ -3534,12 +3539,103 @@ class TestMyUserAdmin(TestCase):
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
|
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||||
("Permissions", {"fields": ("is_active", "groups")}),
|
("Permissions", {"fields": ("is_active", "groups")}),
|
||||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
)
|
)
|
||||||
self.assertEqual(fieldsets, expected_fieldsets)
|
self.assertEqual(fieldsets, expected_fieldsets)
|
||||||
|
|
||||||
|
def test_analyst_can_see_related_domains_and_requests_in_user_form(self):
|
||||||
|
"""Tests if an analyst can see the related domains and domain requests for a user in that user's form"""
|
||||||
|
|
||||||
|
# From MockDb, we have self.meoward_user which we'll use as creator
|
||||||
|
# Create fake domain requests
|
||||||
|
domain_request_started = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED, user=self.meoward_user, name="started.gov"
|
||||||
|
)
|
||||||
|
domain_request_submitted = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.meoward_user, name="submitted.gov"
|
||||||
|
)
|
||||||
|
domain_request_in_review = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.meoward_user, name="in-review.gov"
|
||||||
|
)
|
||||||
|
domain_request_withdrawn = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=self.meoward_user, name="withdrawn.gov"
|
||||||
|
)
|
||||||
|
domain_request_approved = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.APPROVED, user=self.meoward_user, name="approved.gov"
|
||||||
|
)
|
||||||
|
domain_request_rejected = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.REJECTED, user=self.meoward_user, name="rejected.gov"
|
||||||
|
)
|
||||||
|
domain_request_ineligible = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.INELIGIBLE, user=self.meoward_user, name="ineligible.gov"
|
||||||
|
)
|
||||||
|
|
||||||
|
# From MockDb, we have sel.meoward_user who's admin on
|
||||||
|
# self.domain_1 - READY
|
||||||
|
# self.domain_2 - DNS_NEEDED
|
||||||
|
# self.domain_11 - READY
|
||||||
|
# self.domain_12 - READY
|
||||||
|
# DELETED:
|
||||||
|
domain_deleted, _ = Domain.objects.get_or_create(
|
||||||
|
name="domain_deleted.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2024, 4, 2))
|
||||||
|
)
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.meoward_user, domain=domain_deleted, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/user/{}/change/".format(self.meoward_user.id),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the page loaded and contains the expected domain request names and links to the domain requests
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, domain_request_submitted.requested_domain.name)
|
||||||
|
expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_submitted.pk])
|
||||||
|
self.assertContains(response, expected_href)
|
||||||
|
|
||||||
|
self.assertContains(response, domain_request_in_review.requested_domain.name)
|
||||||
|
expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_in_review.pk])
|
||||||
|
self.assertContains(response, expected_href)
|
||||||
|
|
||||||
|
self.assertContains(response, domain_request_approved.requested_domain.name)
|
||||||
|
expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_approved.pk])
|
||||||
|
self.assertContains(response, expected_href)
|
||||||
|
|
||||||
|
self.assertContains(response, domain_request_rejected.requested_domain.name)
|
||||||
|
expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_rejected.pk])
|
||||||
|
self.assertContains(response, expected_href)
|
||||||
|
|
||||||
|
self.assertContains(response, domain_request_ineligible.requested_domain.name)
|
||||||
|
expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_ineligible.pk])
|
||||||
|
self.assertContains(response, expected_href)
|
||||||
|
|
||||||
|
# We filter out those requests
|
||||||
|
# STARTED
|
||||||
|
self.assertNotContains(response, domain_request_started.requested_domain.name)
|
||||||
|
expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_started.pk])
|
||||||
|
self.assertNotContains(response, expected_href)
|
||||||
|
|
||||||
|
# WITHDRAWN
|
||||||
|
self.assertNotContains(response, domain_request_withdrawn.requested_domain.name)
|
||||||
|
expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_withdrawn.pk])
|
||||||
|
self.assertNotContains(response, expected_href)
|
||||||
|
|
||||||
|
# Make sure the page contains the expected domain names and links to the domains
|
||||||
|
self.assertContains(response, self.domain_1.name)
|
||||||
|
expected_href = reverse("admin:registrar_domain_change", args=[self.domain_1.pk])
|
||||||
|
self.assertContains(response, expected_href)
|
||||||
|
|
||||||
|
# We filter out DELETED
|
||||||
|
self.assertNotContains(response, domain_deleted.name)
|
||||||
|
expected_href = reverse("admin:registrar_domain_change", args=[domain_deleted.pk])
|
||||||
|
self.assertNotContains(response, expected_href)
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdminTest(TestCase):
|
class AuditedAdminTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -23,15 +23,32 @@ class TestDataUpdates(TestCase):
|
||||||
self.bs_user = User.objects.create()
|
self.bs_user = User.objects.create()
|
||||||
|
|
||||||
self.contact1 = Contact.objects.create(
|
self.contact1 = Contact.objects.create(
|
||||||
user=self.user1, email="email1@igorville.gov", first_name="first1", last_name="last1"
|
user=self.user1,
|
||||||
|
email="email1@igorville.gov",
|
||||||
|
first_name="first1",
|
||||||
|
last_name="last1",
|
||||||
|
middle_name="middle1",
|
||||||
|
title="title1",
|
||||||
)
|
)
|
||||||
self.contact2 = Contact.objects.create(
|
self.contact2 = Contact.objects.create(
|
||||||
user=self.user2, email="email2@igorville.gov", first_name="first2", last_name="last2"
|
user=self.user2,
|
||||||
|
email="email2@igorville.gov",
|
||||||
|
first_name="first2",
|
||||||
|
last_name="last2",
|
||||||
|
middle_name="middle2",
|
||||||
|
title="title2",
|
||||||
)
|
)
|
||||||
self.contact3 = Contact.objects.create(
|
self.contact3 = Contact.objects.create(
|
||||||
user=self.user3, email="email3@igorville.gov", first_name="first3", last_name="last3"
|
user=self.user3,
|
||||||
|
email="email3@igorville.gov",
|
||||||
|
first_name="first3",
|
||||||
|
last_name="last3",
|
||||||
|
middle_name="middle3",
|
||||||
|
title="title3",
|
||||||
|
)
|
||||||
|
self.contact4 = Contact.objects.create(
|
||||||
|
email="email4@igorville.gov", first_name="first4", last_name="last4", middle_name="middle4", title="title4"
|
||||||
)
|
)
|
||||||
self.contact4 = Contact.objects.create(email="email4@igorville.gov", first_name="first4", last_name="last4")
|
|
||||||
|
|
||||||
self.command = Command()
|
self.command = Command()
|
||||||
|
|
||||||
|
@ -42,14 +59,15 @@ class TestDataUpdates(TestCase):
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
def test_script_updates_linked_users(self):
|
def test_script_updates_linked_users(self):
|
||||||
"""Test the script that copies contacts' first and last names into associated users that
|
"""Test the script that copies contact information to the user object"""
|
||||||
are eligible (first or last are blank or undefined)"""
|
|
||||||
|
|
||||||
# Set up the users' first and last names here so
|
# Set up the users' first and last names here so
|
||||||
# they that they don't get overwritten by Contact's save()
|
# they that they don't get overwritten by Contact's save()
|
||||||
# User with no first or last names
|
# User with no first or last names
|
||||||
self.user1.first_name = ""
|
self.user1.first_name = ""
|
||||||
self.user1.last_name = ""
|
self.user1.last_name = ""
|
||||||
|
self.user1.title = "dummytitle"
|
||||||
|
self.user1.middle_name = "dummymiddle"
|
||||||
self.user1.save()
|
self.user1.save()
|
||||||
|
|
||||||
# User with a first name but no last name
|
# User with a first name but no last name
|
||||||
|
@ -87,12 +105,20 @@ class TestDataUpdates(TestCase):
|
||||||
# The user that has no first and last names will get them from the contact
|
# The user that has no first and last names will get them from the contact
|
||||||
self.assertEqual(self.user1.first_name, "first1")
|
self.assertEqual(self.user1.first_name, "first1")
|
||||||
self.assertEqual(self.user1.last_name, "last1")
|
self.assertEqual(self.user1.last_name, "last1")
|
||||||
# The user that has a first but no last will be left alone
|
self.assertEqual(self.user1.middle_name, "middle1")
|
||||||
self.assertEqual(self.user2.first_name, "First name but no last name")
|
self.assertEqual(self.user1.title, "title1")
|
||||||
self.assertEqual(self.user2.last_name, "")
|
# The user that has a first but no last will be updated
|
||||||
# The user that has a first and a last will be left alone
|
self.assertEqual(self.user2.first_name, "first2")
|
||||||
self.assertEqual(self.user3.first_name, "An existing first name")
|
self.assertEqual(self.user2.last_name, "last2")
|
||||||
self.assertEqual(self.user3.last_name, "An existing last name")
|
self.assertEqual(self.user2.middle_name, "middle2")
|
||||||
|
self.assertEqual(self.user2.title, "title2")
|
||||||
|
# The user that has a first and a last will be updated
|
||||||
|
self.assertEqual(self.user3.first_name, "first3")
|
||||||
|
self.assertEqual(self.user3.last_name, "last3")
|
||||||
|
self.assertEqual(self.user3.middle_name, "middle3")
|
||||||
|
self.assertEqual(self.user3.title, "title3")
|
||||||
# The unlinked user will be left alone
|
# The unlinked user will be left alone
|
||||||
self.assertEqual(self.user4.first_name, "")
|
self.assertEqual(self.user4.first_name, "")
|
||||||
self.assertEqual(self.user4.last_name, "")
|
self.assertEqual(self.user4.last_name, "")
|
||||||
|
self.assertEqual(self.user4.middle_name, None)
|
||||||
|
self.assertEqual(self.user4.title, None)
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from waffle.testutils import override_flag
|
||||||
|
from registrar.utility import email
|
||||||
|
from registrar.utility.email import send_templated_email
|
||||||
from .common import completed_domain_request, less_console_noise
|
from .common import completed_domain_request, less_console_noise
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from registrar.utility import email
|
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +17,24 @@ class TestEmails(TestCase):
|
||||||
self.mock_client_class = MagicMock()
|
self.mock_client_class = MagicMock()
|
||||||
self.mock_client = self.mock_client_class.return_value
|
self.mock_client = self.mock_client_class.return_value
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
@override_flag("disable_email_sending", active=True)
|
||||||
|
def test_disable_email_flag(self):
|
||||||
|
"""Test if the 'disable_email_sending' stops emails from being sent"""
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||||
|
expected_message = "Email sending is disabled due to"
|
||||||
|
with self.assertRaisesRegex(email.EmailSendingError, expected_message):
|
||||||
|
send_templated_email(
|
||||||
|
"test content",
|
||||||
|
"test subject",
|
||||||
|
"doesnotexist@igorville.com",
|
||||||
|
context={"domain_request": self},
|
||||||
|
bcc_address=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that an email wasn't sent
|
||||||
|
self.assertFalse(self.mock_client.send_email.called)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_submission_confirmation(self):
|
def test_submission_confirmation(self):
|
||||||
"""Submission confirmation email works."""
|
"""Submission confirmation email works."""
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Contact,
|
Contact,
|
||||||
|
@ -1602,3 +1603,369 @@ class TestDomainInformationCustomSave(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(domain_information_election.is_election_board, True)
|
self.assertEqual(domain_information_election.is_election_board, True)
|
||||||
self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomainRequestIncomplete(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
username = "test_user"
|
||||||
|
first_name = "First"
|
||||||
|
last_name = "Last"
|
||||||
|
email = "info@example.com"
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
username=username, first_name=first_name, last_name=last_name, email=email
|
||||||
|
)
|
||||||
|
ao, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Meowy",
|
||||||
|
last_name="Meoward",
|
||||||
|
title="Chief Cat",
|
||||||
|
email="meoward@chiefcat.com",
|
||||||
|
phone="(206) 206 2060",
|
||||||
|
)
|
||||||
|
draft_domain, _ = DraftDomain.objects.get_or_create(name="MeowardMeowardMeoward.gov")
|
||||||
|
you, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy you",
|
||||||
|
last_name="Tester you",
|
||||||
|
title="Admin Tester",
|
||||||
|
email="testy-admin@town.com",
|
||||||
|
phone="(555) 555 5556",
|
||||||
|
)
|
||||||
|
other, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy2",
|
||||||
|
last_name="Tester2",
|
||||||
|
title="Another Tester",
|
||||||
|
email="testy2@town.com",
|
||||||
|
phone="(555) 555 5557",
|
||||||
|
)
|
||||||
|
alt, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward1.gov")
|
||||||
|
current, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward.com")
|
||||||
|
self.domain_request = DomainRequest.objects.create(
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_type="executive",
|
||||||
|
federal_agency=FederalAgency.objects.get(agency="AMTRAK"),
|
||||||
|
about_your_organization="Some description",
|
||||||
|
is_election_board=True,
|
||||||
|
tribe_name="Some tribe name",
|
||||||
|
organization_name="Some organization",
|
||||||
|
address_line1="address 1",
|
||||||
|
state_territory="CA",
|
||||||
|
zipcode="94044",
|
||||||
|
authorizing_official=ao,
|
||||||
|
requested_domain=draft_domain,
|
||||||
|
purpose="Some purpose",
|
||||||
|
submitter=you,
|
||||||
|
no_other_contacts_rationale=None,
|
||||||
|
has_cisa_representative=True,
|
||||||
|
cisa_representative_email="somerep@cisa.com",
|
||||||
|
has_anything_else_text=True,
|
||||||
|
anything_else="Anything else",
|
||||||
|
is_policy_acknowledged=True,
|
||||||
|
creator=self.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.domain_request.other_contacts.add(other)
|
||||||
|
self.domain_request.current_websites.add(current)
|
||||||
|
self.domain_request.alternative_domains.add(alt)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
|
def test_is_federal_complete(self):
|
||||||
|
self.assertTrue(self.domain_request._is_federal_complete())
|
||||||
|
self.domain_request.federal_type = None
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertFalse(self.domain_request._is_federal_complete())
|
||||||
|
|
||||||
|
def test_is_interstate_complete(self):
|
||||||
|
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
|
||||||
|
self.domain_request.about_your_organization = "Something something about your organization"
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertTrue(self.domain_request._is_interstate_complete())
|
||||||
|
self.domain_request.about_your_organization = None
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertFalse(self.domain_request._is_interstate_complete())
|
||||||
|
|
||||||
|
def test_is_state_or_territory_complete(self):
|
||||||
|
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
|
||||||
|
self.domain_request.is_election_board = True
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertTrue(self.domain_request._is_state_or_territory_complete())
|
||||||
|
self.domain_request.is_election_board = None
|
||||||
|
self.domain_request.save()
|
||||||
|
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||||
|
self.assertTrue(self.domain_request._is_state_or_territory_complete())
|
||||||
|
|
||||||
|
def test_is_tribal_complete(self):
|
||||||
|
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.TRIBAL
|
||||||
|
self.domain_request.tribe_name = "Tribe Name"
|
||||||
|
self.domain_request.is_election_board = False
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertTrue(self.domain_request._is_tribal_complete())
|
||||||
|
self.domain_request.tribe_name = None
|
||||||
|
self.domain_request.is_election_board = None
|
||||||
|
self.domain_request.save()
|
||||||
|
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||||
|
self.assertFalse(self.domain_request._is_tribal_complete())
|
||||||
|
|
||||||
|
def test_is_county_complete(self):
|
||||||
|
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY
|
||||||
|
self.domain_request.is_election_board = False
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertTrue(self.domain_request._is_county_complete())
|
||||||
|
self.domain_request.is_election_board = None
|
||||||
|
self.domain_request.save()
|
||||||
|
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||||
|
self.assertTrue(self.domain_request._is_county_complete())
|
||||||
|
|
||||||
|
def test_is_city_complete(self):
|
||||||
|
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY
|
||||||
|
self.domain_request.is_election_board = False
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertTrue(self.domain_request._is_city_complete())
|
||||||
|
self.domain_request.is_election_board = None
|
||||||
|
self.domain_request.save()
|
||||||
|
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||||
|
self.assertTrue(self.domain_request._is_city_complete())
|
||||||
|
|
||||||
|
def test_is_special_district_complete(self):
|
||||||
|
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
|
||||||
|
self.domain_request.about_your_organization = "Something something about your organization"
|
||||||
|
self.domain_request.is_election_board = False
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertTrue(self.domain_request._is_special_district_complete())
|
||||||
|
self.domain_request.about_your_organization = None
|
||||||
|
self.domain_request.is_election_board = None
|
||||||
|
self.domain_request.save()
|
||||||
|
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||||
|
self.assertFalse(self.domain_request._is_special_district_complete())
|
||||||
|
|
||||||
|
def test_is_organization_name_and_address_complete(self):
|
||||||
|
self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
|
||||||
|
self.domain_request.organization_name = None
|
||||||
|
self.domain_request.address_line1 = None
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
|
||||||
|
|
||||||
|
def test_is_authorizing_official_complete(self):
|
||||||
|
self.assertTrue(self.domain_request._is_authorizing_official_complete())
|
||||||
|
self.domain_request.authorizing_official = None
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertFalse(self.domain_request._is_authorizing_official_complete())
|
||||||
|
|
||||||
|
def test_is_requested_domain_complete(self):
|
||||||
|
self.assertTrue(self.domain_request._is_requested_domain_complete())
|
||||||
|
self.domain_request.requested_domain = None
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertFalse(self.domain_request._is_requested_domain_complete())
|
||||||
|
|
||||||
|
def test_is_purpose_complete(self):
|
||||||
|
self.assertTrue(self.domain_request._is_purpose_complete())
|
||||||
|
self.domain_request.purpose = None
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertFalse(self.domain_request._is_purpose_complete())
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
def test_is_other_contacts_complete_missing_one_field(self):
|
||||||
|
self.assertTrue(self.domain_request._is_other_contacts_complete())
|
||||||
|
contact = self.domain_request.other_contacts.first()
|
||||||
|
contact.first_name = None
|
||||||
|
contact.save()
|
||||||
|
self.assertFalse(self.domain_request._is_other_contacts_complete())
|
||||||
|
|
||||||
|
def test_is_other_contacts_complete_all_none(self):
|
||||||
|
self.domain_request.other_contacts.clear()
|
||||||
|
self.assertFalse(self.domain_request._is_other_contacts_complete())
|
||||||
|
|
||||||
|
def test_is_other_contacts_False_and_has_rationale(self):
|
||||||
|
# Click radio button "No" for no other contacts and give rationale
|
||||||
|
self.domain_request.other_contacts.clear()
|
||||||
|
self.domain_request.other_contacts.exists = False
|
||||||
|
self.domain_request.no_other_contacts_rationale = "Some rationale"
|
||||||
|
self.assertTrue(self.domain_request._is_other_contacts_complete())
|
||||||
|
|
||||||
|
def test_is_other_contacts_False_and_NO_rationale(self):
|
||||||
|
# Click radio button "No" for no other contacts and DONT give rationale
|
||||||
|
self.domain_request.other_contacts.clear()
|
||||||
|
self.domain_request.other_contacts.exists = False
|
||||||
|
self.domain_request.no_other_contacts_rationale = None
|
||||||
|
self.assertFalse(self.domain_request._is_other_contacts_complete())
|
||||||
|
|
||||||
|
def test_is_additional_details_complete(self):
|
||||||
|
test_cases = [
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - Yes
|
||||||
|
# Anything Else Radio - Yes
|
||||||
|
# Anything Else Text - Yes
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_email": "some@cisarepemail.com",
|
||||||
|
"has_anything_else_text": True,
|
||||||
|
"anything_else": "Some text",
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - Yes
|
||||||
|
# Anything Else Radio - Yes
|
||||||
|
# Anything Else Text - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_email": "some@cisarepemail.com",
|
||||||
|
"has_anything_else_text": True,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - Yes
|
||||||
|
# Anything Else Radio - No
|
||||||
|
# Anything Else Text - No
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_email": "some@cisarepemail.com",
|
||||||
|
"has_anything_else_text": False,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - Yes
|
||||||
|
# Anything Else Radio - None
|
||||||
|
# Anything Else Text - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_email": "some@cisarepemail.com",
|
||||||
|
"has_anything_else_text": None,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": False,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - None
|
||||||
|
# Anything Else Radio - None
|
||||||
|
# Anything Else Text - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": None,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": False,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - None
|
||||||
|
# Anything Else Radio - No
|
||||||
|
# Anything Else Text - No
|
||||||
|
# sync_yes_no will override has_cisa_representative to be False if cisa_representative_email is None
|
||||||
|
# therefore, our expected will be True
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": False,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - None
|
||||||
|
# Anything Else Radio - Yes
|
||||||
|
# Anything Else Text - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": True,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Email - None
|
||||||
|
# Anything Else Radio - Yes
|
||||||
|
# Anything Else Text - Yes
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": True,
|
||||||
|
"anything_else": "Some text",
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - No
|
||||||
|
# Anything Else Radio - Yes
|
||||||
|
# Anything Else Text - Yes
|
||||||
|
{
|
||||||
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": True,
|
||||||
|
"anything_else": "Some text",
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - No
|
||||||
|
# Anything Else Radio - Yes
|
||||||
|
# Anything Else Text - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": True,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - No
|
||||||
|
# Anything Else Radio - None
|
||||||
|
# Anything Else Text - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": None,
|
||||||
|
"anything_else": None,
|
||||||
|
# Above is both None, so it does NOT get overwritten
|
||||||
|
"expected": False,
|
||||||
|
},
|
||||||
|
# CISA Rep - No
|
||||||
|
# Anything Else Radio - No
|
||||||
|
# Anything Else Text - No
|
||||||
|
{
|
||||||
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": False,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": True,
|
||||||
|
},
|
||||||
|
# CISA Rep - None
|
||||||
|
# Anything Else Radio - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": None,
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": None,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for case in test_cases:
|
||||||
|
with self.subTest(case=case):
|
||||||
|
self.domain_request.has_cisa_representative = case["has_cisa_representative"]
|
||||||
|
self.domain_request.cisa_representative_email = case["cisa_representative_email"]
|
||||||
|
self.domain_request.has_anything_else_text = case["has_anything_else_text"]
|
||||||
|
self.domain_request.anything_else = case["anything_else"]
|
||||||
|
self.domain_request.save()
|
||||||
|
self.domain_request.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
self.domain_request._is_additional_details_complete(),
|
||||||
|
case["expected"],
|
||||||
|
msg=f"Failed for case: {case}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_is_policy_acknowledgement_complete(self):
|
||||||
|
self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
|
||||||
|
self.domain_request.is_policy_acknowledged = False
|
||||||
|
self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
|
||||||
|
self.domain_request.is_policy_acknowledged = None
|
||||||
|
self.assertFalse(self.domain_request._is_policy_acknowledgement_complete())
|
||||||
|
|
||||||
|
def test_form_complete(self):
|
||||||
|
self.assertTrue(self.domain_request._form_complete())
|
||||||
|
self.domain_request.generic_org_type = None
|
||||||
|
self.domain_request.save()
|
||||||
|
self.assertFalse(self.domain_request._form_complete())
|
||||||
|
|
|
@ -20,6 +20,7 @@ from django.urls import reverse
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
DomainRequest,
|
DomainRequest,
|
||||||
DomainInformation,
|
DomainInformation,
|
||||||
|
Website,
|
||||||
)
|
)
|
||||||
from waffle.testutils import override_flag
|
from waffle.testutils import override_flag
|
||||||
import logging
|
import logging
|
||||||
|
@ -54,8 +55,19 @@ class TestWithUser(MockEppLib):
|
||||||
first_name = "First"
|
first_name = "First"
|
||||||
last_name = "Last"
|
last_name = "Last"
|
||||||
email = "info@example.com"
|
email = "info@example.com"
|
||||||
|
phone = "8003111234"
|
||||||
self.user = get_user_model().objects.create(
|
self.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, phone=phone
|
||||||
|
)
|
||||||
|
title = "test title"
|
||||||
|
self.user.contact.title = title
|
||||||
|
self.user.contact.save()
|
||||||
|
|
||||||
|
username_incomplete = "test_user_incomplete"
|
||||||
|
first_name_2 = "Incomplete"
|
||||||
|
email_2 = "unicorn@igorville.com"
|
||||||
|
self.incomplete_user = get_user_model().objects.create(
|
||||||
|
username=username_incomplete, first_name=first_name_2, email=email_2
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -64,6 +76,7 @@ class TestWithUser(MockEppLib):
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
self.user.delete()
|
self.user.delete()
|
||||||
|
self.incomplete_user.delete()
|
||||||
|
|
||||||
|
|
||||||
class TestEnvironmentVariablesEffects(TestCase):
|
class TestEnvironmentVariablesEffects(TestCase):
|
||||||
|
@ -161,31 +174,14 @@ class HomeTests(TestWithUser):
|
||||||
self.assertContains(response, "You don't have any registered domains.")
|
self.assertContains(response, "You don't have any registered domains.")
|
||||||
self.assertContains(response, "Why don't I see my domain when I sign in to the registrar?")
|
self.assertContains(response, "Why don't I see my domain when I sign in to the registrar?")
|
||||||
|
|
||||||
def test_home_lists_domain_requests(self):
|
|
||||||
response = self.client.get("/")
|
|
||||||
self.assertNotContains(response, "igorville.gov")
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
|
||||||
domain_request = DomainRequest.objects.create(creator=self.user, requested_domain=site)
|
|
||||||
response = self.client.get("/")
|
|
||||||
|
|
||||||
# count = 7 because of screenreader content
|
|
||||||
self.assertContains(response, "igorville.gov", count=7)
|
|
||||||
|
|
||||||
# clean up
|
|
||||||
domain_request.delete()
|
|
||||||
|
|
||||||
def test_state_help_text(self):
|
def test_state_help_text(self):
|
||||||
"""Tests if each domain state has help text"""
|
"""Tests if each domain state has help text"""
|
||||||
|
|
||||||
# Get the expected text content of each state
|
# Get the expected text content of each state
|
||||||
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
||||||
dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses."
|
dns_needed_text = "Before this domain can be used, "
|
||||||
ready_text = "This domain has name servers and is ready for use."
|
ready_text = "This domain has name servers and is ready for use."
|
||||||
on_hold_text = (
|
on_hold_text = "This domain is administratively paused, "
|
||||||
"This domain is administratively paused, "
|
|
||||||
"so it can’t be edited and won’t resolve in DNS. "
|
|
||||||
"Contact help@get.gov for details."
|
|
||||||
)
|
|
||||||
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
|
||||||
# Generate a mapping of domain names, the state, and expected messages for the subtest
|
# Generate a mapping of domain names, the state, and expected messages for the subtest
|
||||||
test_cases = [
|
test_cases = [
|
||||||
|
@ -206,12 +202,11 @@ class HomeTests(TestWithUser):
|
||||||
user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER
|
user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
# Grab the home page
|
# Grab the json response for domain list
|
||||||
response = self.client.get("/")
|
response = self.client.get("/get-domains-json/")
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
# Make sure the domain is in the list.
|
||||||
# We expect two instances because of SR content.
|
self.assertContains(response, domain_name, count=1)
|
||||||
self.assertContains(response, domain_name, count=2)
|
|
||||||
|
|
||||||
# Check that we have the right text content.
|
# Check that we have the right text content.
|
||||||
self.assertContains(response, expected_message, count=1)
|
self.assertContains(response, expected_message, count=1)
|
||||||
|
@ -222,19 +217,18 @@ class HomeTests(TestWithUser):
|
||||||
|
|
||||||
def test_state_help_text_expired(self):
|
def test_state_help_text_expired(self):
|
||||||
"""Tests if each domain state has help text when expired"""
|
"""Tests if each domain state has help text when expired"""
|
||||||
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
expired_text = "This domain has expired, but it is still online. "
|
||||||
test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY)
|
test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY)
|
||||||
test_domain.expiration_date = date(2011, 10, 10)
|
test_domain.expiration_date = date(2011, 10, 10)
|
||||||
test_domain.save()
|
test_domain.save()
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
# Grab the home page
|
# Grab the json response of the domains list
|
||||||
response = self.client.get("/")
|
response = self.client.get("/get-domains-json/")
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
# Make sure the domain is in the response
|
||||||
# We expect two instances because of SR content.
|
self.assertContains(response, "expired.gov", count=1)
|
||||||
self.assertContains(response, "expired.gov", count=2)
|
|
||||||
|
|
||||||
# Check that we have the right text content.
|
# Check that we have the right text content.
|
||||||
self.assertContains(response, expired_text, count=1)
|
self.assertContains(response, expired_text, count=1)
|
||||||
|
@ -243,19 +237,18 @@ class HomeTests(TestWithUser):
|
||||||
"""Tests if each domain state has help text when expiration date is None"""
|
"""Tests if each domain state has help text when expiration date is None"""
|
||||||
|
|
||||||
# == Test a expiration of None for state ready. This should be expired. == #
|
# == Test a expiration of None for state ready. This should be expired. == #
|
||||||
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
expired_text = "This domain has expired, but it is still online. "
|
||||||
test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY)
|
test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY)
|
||||||
test_domain.expiration_date = None
|
test_domain.expiration_date = None
|
||||||
test_domain.save()
|
test_domain.save()
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
# Grab the home page
|
# Grab the json response of the domains list
|
||||||
response = self.client.get("/")
|
response = self.client.get("/get-domains-json/")
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
# Make sure domain is in the response
|
||||||
# We expect two instances because of SR content.
|
self.assertContains(response, "imexpired.gov", count=1)
|
||||||
self.assertContains(response, "imexpired.gov", count=2)
|
|
||||||
|
|
||||||
# Make sure the expiration date is None
|
# Make sure the expiration date is None
|
||||||
self.assertEqual(test_domain.expiration_date, None)
|
self.assertEqual(test_domain.expiration_date, None)
|
||||||
|
@ -264,19 +257,18 @@ class HomeTests(TestWithUser):
|
||||||
self.assertContains(response, expired_text, count=1)
|
self.assertContains(response, expired_text, count=1)
|
||||||
|
|
||||||
# == Test a expiration of None for state unknown. This should not display expired text. == #
|
# == Test a expiration of None for state unknown. This should not display expired text. == #
|
||||||
unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses."
|
unknown_text = "Before this domain can be used, "
|
||||||
test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN)
|
test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN)
|
||||||
test_domain_2.expiration_date = None
|
test_domain_2.expiration_date = None
|
||||||
test_domain_2.save()
|
test_domain_2.save()
|
||||||
|
|
||||||
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER)
|
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER)
|
||||||
|
|
||||||
# Grab the home page
|
# Grab the json response of the domains list
|
||||||
response = self.client.get("/")
|
response = self.client.get("/get-domains-json/")
|
||||||
|
|
||||||
# Make sure the user can actually see the domain.
|
# Make sure the response contains the domain
|
||||||
# We expect two instances because of SR content.
|
self.assertContains(response, "notexpired.gov", count=1)
|
||||||
self.assertContains(response, "notexpired.gov", count=2)
|
|
||||||
|
|
||||||
# Make sure the expiration date is None
|
# Make sure the expiration date is None
|
||||||
self.assertEqual(test_domain_2.expiration_date, None)
|
self.assertEqual(test_domain_2.expiration_date, None)
|
||||||
|
@ -292,14 +284,6 @@ class HomeTests(TestWithUser):
|
||||||
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.WITHDRAWN
|
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.WITHDRAWN
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure that igorville.gov exists on the page
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
|
|
||||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
|
||||||
self.assertContains(home_page, "Delete")
|
|
||||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
# Trigger the delete logic
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
@ -316,14 +300,6 @@ class HomeTests(TestWithUser):
|
||||||
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.STARTED
|
creator=self.user, requested_domain=site, status=DomainRequest.DomainRequestStatus.STARTED
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure that igorville.gov exists on the page
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
|
|
||||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
|
||||||
self.assertContains(home_page, "Delete")
|
|
||||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
|
||||||
|
|
||||||
# Trigger the delete logic
|
# Trigger the delete logic
|
||||||
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
|
||||||
|
|
||||||
|
@ -511,6 +487,137 @@ class HomeTests(TestWithUser):
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
|
"""A series of tests that target the finish setup page for user profile"""
|
||||||
|
|
||||||
|
# csrf checks do not work well with WebTest.
|
||||||
|
# We disable them here.
|
||||||
|
csrf_checks = False
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user.title = None
|
||||||
|
self.user.save()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.domain, _ = Domain.objects.get_or_create(name="sampledomain.gov", state=Domain.State.READY)
|
||||||
|
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
PublicContact.objects.filter(domain=self.domain).delete()
|
||||||
|
self.role.delete()
|
||||||
|
self.domain.delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
Website.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
|
def _set_session_cookie(self):
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
def _submit_form_webtest(self, form, follow=False):
|
||||||
|
page = form.submit()
|
||||||
|
self._set_session_cookie()
|
||||||
|
return page.follow() if follow else page
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_new_user_with_profile_feature_on(self):
|
||||||
|
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
|
||||||
|
self.app.set_user(self.incomplete_user.username)
|
||||||
|
with override_flag("profile_feature", active=True):
|
||||||
|
# This will redirect the user to the setup page.
|
||||||
|
# Follow implicity checks if our redirect is working.
|
||||||
|
finish_setup_page = self.app.get(reverse("home")).follow()
|
||||||
|
self._set_session_cookie()
|
||||||
|
|
||||||
|
# Assert that we're on the right page
|
||||||
|
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)
|
||||||
|
|
||||||
|
# We're missing a phone number, so the page should tell us that
|
||||||
|
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||||
|
|
||||||
|
# Check for the name of the save button
|
||||||
|
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||||
|
|
||||||
|
# Add a phone number
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertEqual(save_page.status_code, 200)
|
||||||
|
self.assertContains(save_page, "Your profile has been updated.")
|
||||||
|
|
||||||
|
# Try to navigate back to the home page.
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
|
||||||
|
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
|
||||||
|
|
||||||
|
self.app.set_user(self.incomplete_user.username)
|
||||||
|
with override_flag("profile_feature", active=True):
|
||||||
|
# This will redirect the user to the setup page
|
||||||
|
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
|
||||||
|
self._set_session_cookie()
|
||||||
|
|
||||||
|
# Assert that we're on the right page
|
||||||
|
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)
|
||||||
|
|
||||||
|
# We're missing a phone number, so the page should tell us that
|
||||||
|
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||||
|
|
||||||
|
# Check for the name of the save button
|
||||||
|
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||||
|
|
||||||
|
# Add a phone number
|
||||||
|
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"
|
||||||
|
completed_setup_page = self._submit_form_webtest(finish_setup_page.form, follow=True)
|
||||||
|
|
||||||
|
self.assertEqual(completed_setup_page.status_code, 200)
|
||||||
|
|
||||||
|
# Assert that we're on the domain request page
|
||||||
|
self.assertNotContains(completed_setup_page, "Finish setting up your profile")
|
||||||
|
self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?")
|
||||||
|
|
||||||
|
self.assertContains(completed_setup_page, "You’re about to start your .gov domain request")
|
||||||
|
|
||||||
|
@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 UserProfileTests(TestWithUser, WebTest):
|
class UserProfileTests(TestWithUser, WebTest):
|
||||||
"""A series of tests that target your profile functionality"""
|
"""A series of tests that target your profile functionality"""
|
||||||
|
|
||||||
|
@ -539,7 +646,7 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
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 override_flag("profile_feature", active=True):
|
||||||
with self.assertRaises(Exception):
|
with self.assertRaises(Exception):
|
||||||
response = self.client.get(reverse("home"))
|
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")
|
||||||
|
|
||||||
|
@ -559,44 +666,54 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
def test_home_page_main_nav_with_profile_feature_on(self):
|
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"""
|
"""test that Your profile is in main nav of home page when profile_feature is on"""
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("profile_feature", active=True):
|
||||||
response = self.client.get("/")
|
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_home_page_main_nav_with_profile_feature_off(self):
|
||||||
"""test that Your profile is not in main nav of home page when profile_feature is off"""
|
"""test that Your profile is not in main nav of home page when profile_feature is off"""
|
||||||
with override_flag("profile_feature", active=False):
|
with override_flag("profile_feature", active=False):
|
||||||
response = self.client.get("/")
|
response = self.client.get("/", follow=True)
|
||||||
self.assertNotContains(response, "Your profile")
|
self.assertNotContains(response, "Your profile")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_new_request_main_nav_with_profile_feature_on(self):
|
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"""
|
"""test that Your profile is in main nav of new request when profile_feature is on"""
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("profile_feature", active=True):
|
||||||
response = self.client.get("/request/")
|
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_new_request_main_nav_with_profile_feature_off(self):
|
||||||
"""test that Your profile is not in main nav of new request when profile_feature is off"""
|
"""test that Your profile is not in main nav of new request when profile_feature is off"""
|
||||||
with override_flag("profile_feature", active=False):
|
with override_flag("profile_feature", active=False):
|
||||||
response = self.client.get("/request/")
|
response = self.client.get("/request/", follow=True)
|
||||||
self.assertNotContains(response, "Your profile")
|
self.assertNotContains(response, "Your profile")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_user_profile_main_nav_with_profile_feature_on(self):
|
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"""
|
"""test that Your profile is in main nav of user profile when profile_feature is on"""
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("profile_feature", active=True):
|
||||||
response = self.client.get("/user-profile")
|
response = self.client.get("/user-profile", follow=True)
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_user_profile_returns_404_when_feature_off(self):
|
def test_user_profile_returns_404_when_feature_off(self):
|
||||||
"""test that Your profile returns 404 when profile_feature is off"""
|
"""test that Your profile returns 404 when profile_feature is off"""
|
||||||
with override_flag("profile_feature", active=False):
|
with override_flag("profile_feature", active=False):
|
||||||
response = self.client.get("/user-profile")
|
response = self.client.get("/user-profile", follow=True)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_user_profile_back_button_when_coming_from_domain_request(self):
|
||||||
|
"""tests user profile when profile_feature is on,
|
||||||
|
and when they are redirected from the domain request page"""
|
||||||
|
with override_flag("profile_feature", active=True):
|
||||||
|
response = self.client.get("/user-profile?return_to_request=True")
|
||||||
|
self.assertContains(response, "Your profile")
|
||||||
|
self.assertContains(response, "Go back to your domain request")
|
||||||
|
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_profile_feature_on(self):
|
||||||
"""test that domain detail view when profile_feature is on"""
|
"""test that domain detail view when profile_feature is on"""
|
||||||
|
@ -609,14 +726,14 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
def test_domain_your_contact_information_when_profile_feature_off(self):
|
def test_domain_your_contact_information_when_profile_feature_off(self):
|
||||||
"""test that Your contact information is accessible when profile_feature is off"""
|
"""test that Your contact information is accessible when profile_feature is off"""
|
||||||
with override_flag("profile_feature", active=False):
|
with override_flag("profile_feature", active=False):
|
||||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information")
|
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
||||||
self.assertContains(response, "Your contact information")
|
self.assertContains(response, "Your contact information")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_your_contact_information_when_profile_feature_on(self):
|
def test_domain_your_contact_information_when_profile_feature_on(self):
|
||||||
"""test that Your contact information is not accessible when profile feature is on"""
|
"""test that Your contact information is not accessible when profile feature is on"""
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("profile_feature", active=True):
|
||||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information")
|
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
|
||||||
|
@ -633,9 +750,9 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
submitter=contact_user,
|
submitter=contact_user,
|
||||||
)
|
)
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("profile_feature", active=True):
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}")
|
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw")
|
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
||||||
self.assertContains(response, "Your profile")
|
self.assertContains(response, "Your profile")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -652,9 +769,9 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
submitter=contact_user,
|
submitter=contact_user,
|
||||||
)
|
)
|
||||||
with override_flag("profile_feature", active=False):
|
with override_flag("profile_feature", active=False):
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}")
|
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||||
self.assertNotContains(response, "Your profile")
|
self.assertNotContains(response, "Your profile")
|
||||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw")
|
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
||||||
self.assertNotContains(response, "Your profile")
|
self.assertNotContains(response, "Your profile")
|
||||||
# cleanup
|
# cleanup
|
||||||
domain_request.delete()
|
domain_request.delete()
|
||||||
|
@ -669,13 +786,6 @@ class UserProfileTests(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)
|
||||||
profile_form = profile_page.form
|
profile_form = profile_page.form
|
||||||
profile_page = profile_form.submit()
|
|
||||||
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
# assert that first result contains errors
|
|
||||||
self.assertContains(profile_page, "Enter your title")
|
|
||||||
self.assertContains(profile_page, "Enter your phone number")
|
|
||||||
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()
|
||||||
|
|
|
@ -222,27 +222,6 @@ class TestDomainDetail(TestDomainOverview):
|
||||||
self.assertContains(detail_page, "igorville.gov")
|
self.assertContains(detail_page, "igorville.gov")
|
||||||
self.assertContains(detail_page, "Status")
|
self.assertContains(detail_page, "Status")
|
||||||
|
|
||||||
def test_unknown_domain_does_not_show_as_expired_on_homepage(self):
|
|
||||||
"""An UNKNOWN domain does not show as expired on the homepage.
|
|
||||||
It shows as 'DNS needed'"""
|
|
||||||
# At the time of this test's writing, there are 6 UNKNOWN domains inherited
|
|
||||||
# from constructors. Let's reset.
|
|
||||||
with less_console_noise():
|
|
||||||
Domain.objects.all().delete()
|
|
||||||
UserDomainRole.objects.all().delete()
|
|
||||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertNotContains(home_page, "igorville.gov")
|
|
||||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
|
||||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
igorville = Domain.objects.get(name="igorville.gov")
|
|
||||||
self.assertEquals(igorville.state, Domain.State.UNKNOWN)
|
|
||||||
self.assertNotContains(home_page, "Expired")
|
|
||||||
self.assertContains(home_page, "DNS needed")
|
|
||||||
|
|
||||||
def test_unknown_domain_does_not_show_as_expired_on_detail_page(self):
|
def test_unknown_domain_does_not_show_as_expired_on_detail_page(self):
|
||||||
"""An UNKNOWN domain should not exist on the detail_page anymore.
|
"""An UNKNOWN domain should not exist on the detail_page anymore.
|
||||||
It shows as 'DNS needed'"""
|
It shows as 'DNS needed'"""
|
||||||
|
@ -258,11 +237,9 @@ class TestDomainDetail(TestDomainOverview):
|
||||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
igorville = Domain.objects.get(name="igorville.gov")
|
igorville = Domain.objects.get(name="igorville.gov")
|
||||||
self.assertEquals(igorville.state, Domain.State.UNKNOWN)
|
self.assertEquals(igorville.state, Domain.State.UNKNOWN)
|
||||||
detail_page = home_page.click("Manage", index=0)
|
detail_page = self.app.get(f"/domain/{igorville.id}")
|
||||||
self.assertContains(detail_page, "Expired")
|
self.assertContains(detail_page, "Expired")
|
||||||
|
|
||||||
self.assertNotContains(detail_page, "DNS needed")
|
self.assertNotContains(detail_page, "DNS needed")
|
||||||
|
@ -274,26 +251,18 @@ class TestDomainDetail(TestDomainOverview):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
self.user.status = User.RESTRICTED
|
self.user.status = User.RESTRICTED
|
||||||
self.user.save()
|
self.user.save()
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_domain_detail_allowed_for_on_hold(self):
|
def test_domain_detail_allowed_for_on_hold(self):
|
||||||
"""Test that the domain overview page displays for on hold domain"""
|
"""Test that the domain overview page displays for on hold domain"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertContains(home_page, "on-hold.gov")
|
|
||||||
|
|
||||||
# View domain overview page
|
# View domain overview page
|
||||||
detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id}))
|
detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id}))
|
||||||
self.assertNotContains(detail_page, "Edit")
|
self.assertNotContains(detail_page, "Edit")
|
||||||
|
|
||||||
def test_domain_detail_see_just_nameserver(self):
|
def test_domain_detail_see_just_nameserver(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertContains(home_page, "justnameserver.com")
|
|
||||||
|
|
||||||
# View nameserver on Domain Overview page
|
# View nameserver on Domain Overview page
|
||||||
detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id}))
|
detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id}))
|
||||||
|
|
||||||
|
@ -303,9 +272,6 @@ class TestDomainDetail(TestDomainOverview):
|
||||||
|
|
||||||
def test_domain_detail_see_nameserver_and_ip(self):
|
def test_domain_detail_see_nameserver_and_ip(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertContains(home_page, "nameserverwithip.gov")
|
|
||||||
|
|
||||||
# View nameserver on Domain Overview page
|
# View nameserver on Domain Overview page
|
||||||
detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id}))
|
detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id}))
|
||||||
|
|
||||||
|
@ -1121,7 +1087,7 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
|
||||||
"""Can load domain's authorizing official page."""
|
"""Can load domain's authorizing official page."""
|
||||||
page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
|
page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
|
||||||
# once on the sidebar, once in the title
|
# once on the sidebar, once in the title
|
||||||
self.assertContains(page, "Authorizing official", count=2)
|
self.assertContains(page, "Authorizing official", count=3)
|
||||||
|
|
||||||
def test_domain_authorizing_official_content(self):
|
def test_domain_authorizing_official_content(self):
|
||||||
"""Authorizing official information appears on the page."""
|
"""Authorizing official information appears on the page."""
|
||||||
|
@ -1643,8 +1609,6 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
||||||
management pages share the same permissions class"""
|
management pages share the same permissions class"""
|
||||||
self.user.status = User.RESTRICTED
|
self.user.status = User.RESTRICTED
|
||||||
self.user.save()
|
self.user.save()
|
||||||
home_page = self.app.get("/")
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
151
src/registrar/tests/test_views_domains_json.py
Normal file
151
src/registrar/tests/test_views_domains_json.py
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
from registrar.models import UserDomainRole, Domain
|
||||||
|
from django.urls import reverse
|
||||||
|
from .test_views import TestWithUser
|
||||||
|
from django_webtest import WebTest # type: ignore
|
||||||
|
from django.utils.dateparse import parse_date
|
||||||
|
|
||||||
|
|
||||||
|
class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
# Create test domains
|
||||||
|
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="active")
|
||||||
|
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="inactive")
|
||||||
|
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="active")
|
||||||
|
|
||||||
|
# Create UserDomainRoles
|
||||||
|
UserDomainRole.objects.create(user=self.user, domain=self.domain1)
|
||||||
|
UserDomainRole.objects.create(user=self.user, domain=self.domain2)
|
||||||
|
UserDomainRole.objects.create(user=self.user, domain=self.domain3)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
def test_get_domains_json_unauthenticated(self):
|
||||||
|
"""for an unauthenticated user, test that the user is redirected for auth"""
|
||||||
|
self.app.reset()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("get_domains_json"))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
def test_get_domains_json_authenticated(self):
|
||||||
|
"""Test that an authenticated user gets the list of 3 domains."""
|
||||||
|
response = self.app.get(reverse("get_domains_json"))
|
||||||
|
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"]), 3)
|
||||||
|
|
||||||
|
# Expected domains
|
||||||
|
expected_domains = [self.domain1, self.domain2, 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
|
||||||
|
action_label_expected = (
|
||||||
|
"View"
|
||||||
|
if 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 expected_domains[i].state
|
||||||
|
in [
|
||||||
|
Domain.State.DELETED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
]
|
||||||
|
else "settings"
|
||||||
|
)
|
||||||
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
|
def test_pagination(self):
|
||||||
|
"""Test that pagination is correct in the response"""
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"page": 1})
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_sorting(self):
|
||||||
|
"""test that sorting works properly in the response"""
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"sort_by": "expiration_date", "order": "desc"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check if sorted by expiration_date in descending order
|
||||||
|
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
|
||||||
|
self.assertEqual(expiration_dates, sorted(expiration_dates, reverse=True))
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"sort_by": "expiration_date", "order": "asc"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check if sorted by expiration_date in ascending order
|
||||||
|
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
|
||||||
|
self.assertEqual(expiration_dates, sorted(expiration_dates))
|
||||||
|
|
||||||
|
def test_sorting_by_state_display(self):
|
||||||
|
"""test that the state_display sorting works properly"""
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"sort_by": "state_display", "order": "asc"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check if sorted by state_display in ascending order
|
||||||
|
states = [domain["state_display"] for domain in data["domains"]]
|
||||||
|
self.assertEqual(states, sorted(states))
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"sort_by": "state_display", "order": "desc"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check if sorted by state_display in descending order
|
||||||
|
states = [domain["state_display"] for domain in data["domains"]]
|
||||||
|
self.assertEqual(states, sorted(states, reverse=True))
|
|
@ -47,11 +47,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
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'"""
|
||||||
completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
|
||||||
home_page = self.app.get("/")
|
detail_page = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
||||||
self.assertContains(home_page, "city.gov")
|
|
||||||
# click the "Edit" link
|
|
||||||
detail_page = home_page.click("Edit", index=0)
|
|
||||||
# Check that the response is a redirect
|
# Check that the response is a redirect
|
||||||
self.assertEqual(detail_page.status_code, 302)
|
self.assertEqual(detail_page.status_code, 302)
|
||||||
# You can access the 'Location' header to get the redirect URL
|
# You can access the 'Location' header to get the redirect URL
|
||||||
|
@ -470,6 +467,323 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
# check that any new pages are added to this test
|
# check that any new pages are added to this test
|
||||||
self.assertEqual(num_pages, num_pages_tested)
|
self.assertEqual(num_pages, num_pages_tested)
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_domain_request_form_submission_incomplete(self):
|
||||||
|
num_pages_tested = 0
|
||||||
|
# skipping elections, type_of_work, tribal_government
|
||||||
|
|
||||||
|
intro_page = self.app.get(reverse("domain-request:"))
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
intro_form = intro_page.forms[0]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
intro_result = intro_form.submit()
|
||||||
|
|
||||||
|
# follow first redirect
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_page = intro_result.follow()
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
# ---- TYPE PAGE ----
|
||||||
|
type_form = type_page.forms[0]
|
||||||
|
type_form["generic_org_type-generic_org_type"] = "federal"
|
||||||
|
# test next button and validate data
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_result = type_form.submit()
|
||||||
|
# should see results in db
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.generic_org_type, "federal")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(type_result.status_code, 302)
|
||||||
|
self.assertEqual(type_result["Location"], "/request/organization_federal/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- FEDERAL BRANCH PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
federal_page = type_result.follow()
|
||||||
|
federal_form = federal_page.forms[0]
|
||||||
|
federal_form["organization_federal-federal_type"] = "executive"
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
federal_result = federal_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.federal_type, "executive")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(federal_result.status_code, 302)
|
||||||
|
self.assertEqual(federal_result["Location"], "/request/organization_contact/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- ORG CONTACT PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
org_contact_page = federal_result.follow()
|
||||||
|
org_contact_form = org_contact_page.forms[0]
|
||||||
|
# federal agency so we have to fill in federal_agency
|
||||||
|
federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
|
||||||
|
org_contact_form["organization_contact-federal_agency"] = federal_agency.id
|
||||||
|
org_contact_form["organization_contact-organization_name"] = "Testorg"
|
||||||
|
org_contact_form["organization_contact-address_line1"] = "address 1"
|
||||||
|
org_contact_form["organization_contact-address_line2"] = "address 2"
|
||||||
|
org_contact_form["organization_contact-city"] = "NYC"
|
||||||
|
org_contact_form["organization_contact-state_territory"] = "NY"
|
||||||
|
org_contact_form["organization_contact-zipcode"] = "10002"
|
||||||
|
org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks"
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
org_contact_result = org_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.organization_name, "Testorg")
|
||||||
|
self.assertEqual(domain_request.address_line1, "address 1")
|
||||||
|
self.assertEqual(domain_request.address_line2, "address 2")
|
||||||
|
self.assertEqual(domain_request.city, "NYC")
|
||||||
|
self.assertEqual(domain_request.state_territory, "NY")
|
||||||
|
self.assertEqual(domain_request.zipcode, "10002")
|
||||||
|
self.assertEqual(domain_request.urbanization, "URB Royal Oaks")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(org_contact_result.status_code, 302)
|
||||||
|
self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- AUTHORIZING OFFICIAL PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
ao_page = org_contact_result.follow()
|
||||||
|
ao_form = ao_page.forms[0]
|
||||||
|
ao_form["authorizing_official-first_name"] = "Testy ATO"
|
||||||
|
ao_form["authorizing_official-last_name"] = "Tester ATO"
|
||||||
|
ao_form["authorizing_official-title"] = "Chief Tester"
|
||||||
|
ao_form["authorizing_official-email"] = "testy@town.com"
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
ao_result = ao_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.authorizing_official.first_name, "Testy ATO")
|
||||||
|
self.assertEqual(domain_request.authorizing_official.last_name, "Tester ATO")
|
||||||
|
self.assertEqual(domain_request.authorizing_official.title, "Chief Tester")
|
||||||
|
self.assertEqual(domain_request.authorizing_official.email, "testy@town.com")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(ao_result.status_code, 302)
|
||||||
|
self.assertEqual(ao_result["Location"], "/request/current_sites/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- CURRENT SITES PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
current_sites_page = ao_result.follow()
|
||||||
|
current_sites_form = current_sites_page.forms[0]
|
||||||
|
current_sites_form["current_sites-0-website"] = "www.city.com"
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
current_sites_result = current_sites_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(
|
||||||
|
domain_request.current_websites.filter(website="http://www.city.com").count(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(current_sites_result.status_code, 302)
|
||||||
|
self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- DOTGOV DOMAIN PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
dotgov_page = current_sites_result.follow()
|
||||||
|
dotgov_form = dotgov_page.forms[0]
|
||||||
|
dotgov_form["dotgov_domain-requested_domain"] = "city"
|
||||||
|
dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
dotgov_result = dotgov_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.requested_domain.name, "city.gov")
|
||||||
|
self.assertEqual(domain_request.alternative_domains.filter(website="city1.gov").count(), 1)
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(dotgov_result.status_code, 302)
|
||||||
|
self.assertEqual(dotgov_result["Location"], "/request/purpose/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- PURPOSE PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
purpose_page = dotgov_result.follow()
|
||||||
|
purpose_form = purpose_page.forms[0]
|
||||||
|
purpose_form["purpose-purpose"] = "For all kinds of things."
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
purpose_result = purpose_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.purpose, "For all kinds of things.")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(purpose_result.status_code, 302)
|
||||||
|
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
|
||||||
|
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
|
||||||
|
|
||||||
|
# ---- OTHER CONTACTS PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
other_contacts_page = your_contact_result.follow()
|
||||||
|
|
||||||
|
# This page has 3 forms in 1.
|
||||||
|
# Let's set the yes/no radios to enable the other contacts fieldsets
|
||||||
|
other_contacts_form = other_contacts_page.forms[0]
|
||||||
|
|
||||||
|
other_contacts_form["other_contacts-has_other_contacts"] = "True"
|
||||||
|
|
||||||
|
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
|
||||||
|
other_contacts_form["other_contacts-0-last_name"] = "Tester2"
|
||||||
|
other_contacts_form["other_contacts-0-title"] = "Another Tester"
|
||||||
|
other_contacts_form["other_contacts-0-email"] = "testy2@town.com"
|
||||||
|
other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557"
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
other_contacts_result = other_contacts_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(
|
||||||
|
domain_request.other_contacts.filter(
|
||||||
|
first_name="Testy2",
|
||||||
|
last_name="Tester2",
|
||||||
|
title="Another Tester",
|
||||||
|
email="testy2@town.com",
|
||||||
|
phone="(201) 555 5557",
|
||||||
|
).count(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(other_contacts_result.status_code, 302)
|
||||||
|
self.assertEqual(other_contacts_result["Location"], "/request/additional_details/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- ADDITIONAL DETAILS PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
additional_details_page = other_contacts_result.follow()
|
||||||
|
additional_details_form = additional_details_page.forms[0]
|
||||||
|
|
||||||
|
# load inputs with test data
|
||||||
|
|
||||||
|
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||||
|
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||||
|
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
||||||
|
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
additional_details_result = additional_details_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
||||||
|
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(additional_details_result.status_code, 302)
|
||||||
|
self.assertEqual(additional_details_result["Location"], "/request/requirements/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- REQUIREMENTS PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
requirements_page = additional_details_result.follow()
|
||||||
|
requirements_form = requirements_page.forms[0]
|
||||||
|
|
||||||
|
requirements_form["requirements-is_policy_acknowledged"] = True
|
||||||
|
|
||||||
|
# Before we go to the review page, let's remove some of the data from the request:
|
||||||
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
|
||||||
|
domain_request.generic_org_type = None
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
requirements_result = requirements_form.submit()
|
||||||
|
# validate that data from this step are being saved
|
||||||
|
|
||||||
|
domain_request.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(domain_request.is_policy_acknowledged, True)
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
|
# the domain request page
|
||||||
|
self.assertEqual(requirements_result.status_code, 302)
|
||||||
|
self.assertEqual(requirements_result["Location"], "/request/review/")
|
||||||
|
num_pages_tested += 1
|
||||||
|
|
||||||
|
# ---- REVIEW AND FINSIHED PAGES ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
review_page = requirements_result.follow()
|
||||||
|
# review_form = review_page.forms[0]
|
||||||
|
|
||||||
|
# Review page contains all the previously entered data
|
||||||
|
# Let's make sure the long org name is displayed
|
||||||
|
self.assertNotContains(review_page, "Federal")
|
||||||
|
# self.assertContains(review_page, "Executive")
|
||||||
|
self.assertContains(review_page, "Incomplete", count=1)
|
||||||
|
|
||||||
|
# We can't test the modal itself as it relies on JS for init and triggering,
|
||||||
|
# but we can test for the existence of its trigger:
|
||||||
|
self.assertContains(review_page, "toggle-submit-domain-request")
|
||||||
|
# And the existence of the modal's data parked and ready for the js init.
|
||||||
|
# The next assert also tests for the passed requested domain context from
|
||||||
|
# the view > domain_request_form > modal
|
||||||
|
self.assertNotContains(review_page, "You are about to submit a domain request for city.gov")
|
||||||
|
self.assertContains(review_page, "Your request form is incomplete")
|
||||||
|
|
||||||
# This is the start of a test to check an existing domain_request, it currently
|
# This is the start of a test to check an existing domain_request, it currently
|
||||||
# does not work and results in errors as noted in:
|
# does not work and results in errors as noted in:
|
||||||
# https://github.com/cisagov/getgov/pull/728
|
# https://github.com/cisagov/getgov/pull/728
|
||||||
|
@ -2388,7 +2702,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
review_page = self.app.get(reverse("domain-request:review"))
|
review_page = self.app.get(reverse("domain-request:review"))
|
||||||
self.assertContains(review_page, "toggle-submit-domain-request")
|
self.assertContains(review_page, "toggle-submit-domain-request")
|
||||||
self.assertContains(review_page, "You are about to submit an incomplete request")
|
self.assertContains(review_page, "Your request form is incomplete")
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
|
@ -2402,10 +2716,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
home_page = self.app.get("/")
|
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
self.assertContains(home_page, "city.gov")
|
|
||||||
# click the "Manage" link
|
|
||||||
detail_page = home_page.click("Manage", index=0)
|
|
||||||
self.assertContains(detail_page, "city.gov")
|
self.assertContains(detail_page, "city.gov")
|
||||||
self.assertContains(detail_page, "city1.gov")
|
self.assertContains(detail_page, "city1.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
|
@ -2422,10 +2733,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
home_page = self.app.get("/")
|
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
self.assertContains(home_page, "city.gov")
|
|
||||||
# click the "Manage" link
|
|
||||||
detail_page = home_page.click("Manage", index=0)
|
|
||||||
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")
|
||||||
|
@ -2437,10 +2745,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
home_page = self.app.get("/")
|
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
self.assertContains(home_page, "city.gov")
|
|
||||||
# click the "Manage" link
|
|
||||||
detail_page = home_page.click("Manage", index=0)
|
|
||||||
self.assertContains(detail_page, "city.gov")
|
self.assertContains(detail_page, "city.gov")
|
||||||
self.assertContains(detail_page, "city1.gov")
|
self.assertContains(detail_page, "city1.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
|
@ -2462,8 +2767,8 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
target_status_code=200,
|
target_status_code=200,
|
||||||
fetch_redirect_response=True,
|
fetch_redirect_response=True,
|
||||||
)
|
)
|
||||||
home_page = self.app.get("/")
|
response = self.client.get("/get-domain-requests-json/")
|
||||||
self.assertContains(home_page, "Withdrawn")
|
self.assertContains(response, "Withdrawn")
|
||||||
|
|
||||||
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."""
|
||||||
|
@ -2472,10 +2777,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
home_page = self.app.get("/")
|
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
self.assertContains(home_page, "city.gov")
|
|
||||||
# click the "Manage" link
|
|
||||||
detail_page = home_page.click("Manage", index=0)
|
|
||||||
self.assertContains(detail_page, "city.gov")
|
self.assertContains(detail_page, "city.gov")
|
||||||
self.assertContains(detail_page, "city1.gov")
|
self.assertContains(detail_page, "city1.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
|
@ -2545,9 +2847,9 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
def test_unlocked_steps_full_domain_request(self):
|
def test_unlocked_steps_full_domain_request(self):
|
||||||
"""Test when all fields in the domain request are filled."""
|
"""Test when all fields in the domain request are filled."""
|
||||||
|
|
||||||
completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
|
||||||
# Make a request to the home page
|
|
||||||
home_page = self.app.get("/")
|
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
||||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
# resetting the session key on each new request, thus destroying the concept
|
# 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
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
@ -2555,13 +2857,6 @@ class TestWizardUnlockingSteps(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)
|
||||||
|
|
||||||
# Assert that the response contains "city.gov"
|
|
||||||
self.assertContains(home_page, "city.gov")
|
|
||||||
|
|
||||||
# Click the "Edit" link
|
|
||||||
response = home_page.click("Edit", index=0)
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
|
|
||||||
# Check if the response is a redirect
|
# Check if the response is a redirect
|
||||||
if response.status_code == 302:
|
if response.status_code == 302:
|
||||||
# Follow the redirect manually
|
# Follow the redirect manually
|
||||||
|
@ -2612,8 +2907,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
)
|
)
|
||||||
domain_request.other_contacts.set([contact_2])
|
domain_request.other_contacts.set([contact_2])
|
||||||
|
|
||||||
# Make a request to the home page
|
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
||||||
home_page = self.app.get("/")
|
|
||||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
# resetting the session key on each new request, thus destroying the concept
|
# 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
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
@ -2621,13 +2915,6 @@ class TestWizardUnlockingSteps(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)
|
||||||
|
|
||||||
# Assert that the response contains "city.gov"
|
|
||||||
self.assertContains(home_page, "igorville.gov")
|
|
||||||
|
|
||||||
# Click the "Edit" link
|
|
||||||
response = home_page.click("Edit", index=0)
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
|
|
||||||
# Check if the response is a redirect
|
# Check if the response is a redirect
|
||||||
if response.status_code == 302:
|
if response.status_code == 302:
|
||||||
# Follow the redirect manually
|
# Follow the redirect manually
|
||||||
|
|
248
src/registrar/tests/test_views_requests_json.py
Normal file
248
src/registrar/tests/test_views_requests_json.py
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
from registrar.models import DomainRequest
|
||||||
|
from django.urls import reverse
|
||||||
|
from .test_views import TestWithUser
|
||||||
|
from django_webtest import WebTest # type: ignore
|
||||||
|
from django.utils.dateparse import parse_datetime
|
||||||
|
|
||||||
|
|
||||||
|
class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
# Create domain requests for the user
|
||||||
|
self.domain_requests = [
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-01-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
created_at="2024-01-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-02-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
created_at="2024-02-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-03-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
created_at="2024-03-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-04-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
created_at="2024-04-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-05-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
created_at="2024-05-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-06-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
created_at="2024-06-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-07-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
created_at="2024-07-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-08-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
created_at="2024-08-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-09-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
created_at="2024-09-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-10-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
created_at="2024-10-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-11-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
created_at="2024-11-01",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-11-02",
|
||||||
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
created_at="2024-11-02",
|
||||||
|
),
|
||||||
|
DomainRequest.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=None,
|
||||||
|
submission_date="2024-12-01",
|
||||||
|
status=DomainRequest.DomainRequestStatus.APPROVED,
|
||||||
|
created_at="2024-12-01",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
|
||||||
|
def test_get_domain_requests_json_authenticated(self):
|
||||||
|
"""Test that domain requests are returned properly for an authenticated user."""
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertTrue(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 2)
|
||||||
|
|
||||||
|
# Check the number of domain requests
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 10)
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
requested_domains = [request["requested_domain"] for request in data["domain_requests"]]
|
||||||
|
submission_dates = [request["submission_date"] for request in data["domain_requests"]]
|
||||||
|
statuses = [request["status"] for request in data["domain_requests"]]
|
||||||
|
created_ats = [request["created_at"] for request in data["domain_requests"]]
|
||||||
|
ids = [request["id"] for request in data["domain_requests"]]
|
||||||
|
is_deletables = [request["is_deletable"] for request in data["domain_requests"]]
|
||||||
|
action_urls = [request["action_url"] for request in data["domain_requests"]]
|
||||||
|
action_labels = [request["action_label"] for request in data["domain_requests"]]
|
||||||
|
svg_icons = [request["svg_icon"] for request in data["domain_requests"]]
|
||||||
|
|
||||||
|
# Check fields for each domain request
|
||||||
|
for i in range(10):
|
||||||
|
self.assertNotEqual(data["domain_requests"][i]["status"], "Approved")
|
||||||
|
self.assertEqual(
|
||||||
|
self.domain_requests[i].requested_domain.name if self.domain_requests[i].requested_domain else None,
|
||||||
|
requested_domains[i],
|
||||||
|
)
|
||||||
|
self.assertEqual(self.domain_requests[i].submission_date, submission_dates[i])
|
||||||
|
self.assertEqual(self.domain_requests[i].get_status_display(), statuses[i])
|
||||||
|
self.assertEqual(
|
||||||
|
parse_datetime(self.domain_requests[i].created_at.isoformat()), parse_datetime(created_ats[i])
|
||||||
|
)
|
||||||
|
self.assertEqual(self.domain_requests[i].id, ids[i])
|
||||||
|
|
||||||
|
# Check is_deletable
|
||||||
|
is_deletable_expected = self.domain_requests[i].status in [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
]
|
||||||
|
self.assertEqual(is_deletable_expected, is_deletables[i])
|
||||||
|
|
||||||
|
# Check action_url
|
||||||
|
action_url_expected = (
|
||||||
|
reverse("edit-domain-request", kwargs={"id": self.domain_requests[i].id})
|
||||||
|
if self.domain_requests[i].status
|
||||||
|
in [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
]
|
||||||
|
else reverse("domain-request-status", kwargs={"pk": self.domain_requests[i].id})
|
||||||
|
)
|
||||||
|
self.assertEqual(action_url_expected, action_urls[i])
|
||||||
|
|
||||||
|
# Check action_label
|
||||||
|
action_label_expected = (
|
||||||
|
"Edit"
|
||||||
|
if self.domain_requests[i].status
|
||||||
|
in [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
]
|
||||||
|
else "Manage"
|
||||||
|
)
|
||||||
|
self.assertEqual(action_label_expected, action_labels[i])
|
||||||
|
|
||||||
|
# Check svg_icon
|
||||||
|
svg_icon_expected = (
|
||||||
|
"edit"
|
||||||
|
if self.domain_requests[i].status
|
||||||
|
in [
|
||||||
|
DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||||
|
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
|
]
|
||||||
|
else "settings"
|
||||||
|
)
|
||||||
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
|
def test_pagination(self):
|
||||||
|
"""Test that pagination works properly. There are 11 total non-approved requests and
|
||||||
|
a page size of 10"""
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"page": 1})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertTrue(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 2)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"page": 2})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 2)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertTrue(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 2)
|
||||||
|
|
||||||
|
def test_sorting(self):
|
||||||
|
"""test that sorting works properly on the result set"""
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"sort_by": "submission_date", "order": "desc"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check if sorted by submission_date in descending order
|
||||||
|
submission_dates = [req["submission_date"] for req in data["domain_requests"]]
|
||||||
|
self.assertEqual(submission_dates, sorted(submission_dates, reverse=True))
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"sort_by": "submission_date", "order": "asc"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check if sorted by submission_date in ascending order
|
||||||
|
submission_dates = [req["submission_date"] for req in data["domain_requests"]]
|
||||||
|
self.assertEqual(submission_dates, sorted(submission_dates))
|
||||||
|
|
||||||
|
def test_filter_approved_excluded(self):
|
||||||
|
"""test that approved requests are excluded from result set."""
|
||||||
|
# sort in reverse chronological order of submission date, since most recent request is approved
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"sort_by": "submission_date", "order": "desc"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Ensure no approved requests are included
|
||||||
|
for domain_request in data["domain_requests"]:
|
||||||
|
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
|
@ -455,7 +455,6 @@ def export_data_full_to_csv(csv_file):
|
||||||
|
|
||||||
def export_data_federal_to_csv(csv_file):
|
def export_data_federal_to_csv(csv_file):
|
||||||
"""Federal domains report"""
|
"""Federal domains report"""
|
||||||
|
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
# define columns to include in export
|
# define columns to include in export
|
||||||
columns = [
|
columns = [
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.template.loader import get_template
|
||||||
from email.mime.application import MIMEApplication
|
from email.mime.application import MIMEApplication
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
from waffle import flag_is_active
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -35,6 +36,11 @@ def send_templated_email(
|
||||||
|
|
||||||
Raises EmailSendingError if SES client could not be accessed
|
Raises EmailSendingError if SES client could not be accessed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if flag_is_active(None, "disable_email_sending") and not settings.IS_PRODUCTION: # type: ignore
|
||||||
|
message = "Could not send email. Email sending is disabled due to flag 'disable_email_sending'."
|
||||||
|
raise EmailSendingError(message)
|
||||||
|
|
||||||
template = get_template(template_name)
|
template = get_template(template_name)
|
||||||
email_body = template.render(context=context)
|
email_body = template.render(context=context)
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,6 @@ from .domain import (
|
||||||
DomainInvitationDeleteView,
|
DomainInvitationDeleteView,
|
||||||
DomainDeleteUserView,
|
DomainDeleteUserView,
|
||||||
)
|
)
|
||||||
from .user_profile import UserProfileView
|
from .user_profile import UserProfileView, FinishProfileSetupView
|
||||||
from .health import *
|
from .health import *
|
||||||
from .index import *
|
from .index import *
|
||||||
|
|
|
@ -379,30 +379,45 @@ 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."""
|
||||||
# Build the submit button that we'll pass to the modal.
|
|
||||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
|
||||||
# Concatenate the modal header that we'll pass to the modal.
|
|
||||||
if self.domain_request.requested_domain:
|
|
||||||
modal_heading = "You are about to submit a domain request for " + str(self.domain_request.requested_domain)
|
|
||||||
else:
|
|
||||||
modal_heading = "You are about to submit an incomplete request"
|
|
||||||
|
|
||||||
has_profile_flag = flag_is_active(self.request, "profile_feature")
|
has_profile_flag = flag_is_active(self.request, "profile_feature")
|
||||||
logger.debug("PROFILE FLAG is %s" % has_profile_flag)
|
logger.debug("PROFILE FLAG is %s" % has_profile_flag)
|
||||||
|
|
||||||
context = {
|
context_stuff = {}
|
||||||
"form_titles": self.TITLES,
|
if DomainRequest._form_complete(self.domain_request):
|
||||||
"steps": self.steps,
|
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
||||||
# Add information about which steps should be unlocked
|
context_stuff = {
|
||||||
"visited": self.storage.get("step_history", []),
|
"not_form": False,
|
||||||
"is_federal": self.domain_request.is_federal(),
|
"form_titles": self.TITLES,
|
||||||
"modal_button": modal_button,
|
"steps": self.steps,
|
||||||
"modal_heading": modal_heading,
|
"visited": self.storage.get("step_history", []),
|
||||||
# Use the profile waffle feature flag to toggle profile features throughout domain requests
|
"is_federal": self.domain_request.is_federal(),
|
||||||
"has_profile_feature_flag": has_profile_flag,
|
"modal_button": modal_button,
|
||||||
"user": self.request.user,
|
"modal_heading": "You are about to submit a domain request for "
|
||||||
}
|
+ str(self.domain_request.requested_domain),
|
||||||
return context
|
"modal_description": "Once you submit this request, you won’t be able to edit it until we review it.\
|
||||||
|
You’ll only be able to withdraw your request.",
|
||||||
|
"review_form_is_complete": True,
|
||||||
|
# Use the profile waffle feature flag to toggle profile features throughout domain requests
|
||||||
|
"has_profile_feature_flag": has_profile_flag,
|
||||||
|
"user": self.request.user,
|
||||||
|
}
|
||||||
|
else: # form is not complete
|
||||||
|
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
||||||
|
context_stuff = {
|
||||||
|
"not_form": True,
|
||||||
|
"form_titles": self.TITLES,
|
||||||
|
"steps": self.steps,
|
||||||
|
"visited": self.storage.get("step_history", []),
|
||||||
|
"is_federal": self.domain_request.is_federal(),
|
||||||
|
"modal_button": modal_button,
|
||||||
|
"modal_heading": "Your request form is incomplete",
|
||||||
|
"modal_description": 'This request cannot be submitted yet.\
|
||||||
|
Return to the request and visit the steps that are marked as "incomplete."',
|
||||||
|
"review_form_is_complete": False,
|
||||||
|
"has_profile_feature_flag": has_profile_flag,
|
||||||
|
"user": self.request.user,
|
||||||
|
}
|
||||||
|
return context_stuff
|
||||||
|
|
||||||
def get_step_list(self) -> list:
|
def get_step_list(self) -> list:
|
||||||
"""Dynamically generated list of steps in the form wizard."""
|
"""Dynamically generated list of steps in the form wizard."""
|
||||||
|
@ -670,6 +685,8 @@ class Review(DomainRequestWizard):
|
||||||
forms = [] # type: ignore
|
forms = [] # type: ignore
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
|
if DomainRequest._form_complete(self.domain_request) is False:
|
||||||
|
logger.warning("User arrived at review page with an incomplete form.")
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
context["Step"] = Step.__members__
|
context["Step"] = Step.__members__
|
||||||
context["domain_request"] = self.domain_request
|
context["domain_request"] = self.domain_request
|
||||||
|
|
79
src/registrar/views/domain_requests_json.py
Normal file
79
src/registrar/views/domain_requests_json.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from registrar.models import DomainRequest
|
||||||
|
from django.utils.dateformat import format
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def get_domain_requests_json(request):
|
||||||
|
"""Given the current request,
|
||||||
|
get all domain requests that are associated with the request user and exclude the APPROVED ones"""
|
||||||
|
|
||||||
|
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
||||||
|
status=DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
)
|
||||||
|
# Handle sorting
|
||||||
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
if order == "desc":
|
||||||
|
sort_by = f"-{sort_by}"
|
||||||
|
domain_requests = domain_requests.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 = [
|
||||||
|
{
|
||||||
|
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
|
||||||
|
"submission_date": domain_request.submission_date,
|
||||||
|
"status": domain_request.get_status_display(),
|
||||||
|
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
|
||||||
|
"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(
|
||||||
|
{
|
||||||
|
"domain_requests": domain_requests_data,
|
||||||
|
"has_next": page_obj.has_next(),
|
||||||
|
"has_previous": page_obj.has_previous(),
|
||||||
|
"page": page_obj.number,
|
||||||
|
"num_pages": paginator.num_pages,
|
||||||
|
"total": paginator.count,
|
||||||
|
}
|
||||||
|
)
|
60
src/registrar/views/domains_json.py
Normal file
60
src/registrar/views/domains_json.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from registrar.models import UserDomainRole, Domain
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def get_domains_json(request):
|
||||||
|
"""Given the current request,
|
||||||
|
get all domains that are associated with the UserDomainRole object"""
|
||||||
|
|
||||||
|
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||||
|
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||||
|
|
||||||
|
objects = Domain.objects.filter(id__in=domain_ids)
|
||||||
|
|
||||||
|
# Handle sorting
|
||||||
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
|
||||||
|
if sort_by == "state_display":
|
||||||
|
# Fetch the objects and sort them in Python
|
||||||
|
objects = list(objects) # Evaluate queryset to a list
|
||||||
|
objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc"))
|
||||||
|
else:
|
||||||
|
if order == "desc":
|
||||||
|
sort_by = f"-{sort_by}"
|
||||||
|
objects = objects.order_by(sort_by)
|
||||||
|
|
||||||
|
paginator = Paginator(objects, 10)
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
# Convert objects to JSON-serializable format
|
||||||
|
domains = [
|
||||||
|
{
|
||||||
|
"id": domain.id,
|
||||||
|
"name": domain.name,
|
||||||
|
"expiration_date": domain.expiration_date,
|
||||||
|
"state": domain.state,
|
||||||
|
"state_display": domain.state_display(),
|
||||||
|
"get_state_help_text": domain.get_state_help_text(),
|
||||||
|
"action_url": reverse("domain", kwargs={"pk": domain.id}),
|
||||||
|
"action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"),
|
||||||
|
"svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"),
|
||||||
|
}
|
||||||
|
for domain in page_obj.object_list
|
||||||
|
]
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"domains": domains,
|
||||||
|
"page": page_obj.number,
|
||||||
|
"num_pages": paginator.num_pages,
|
||||||
|
"has_previous": page_obj.has_previous(),
|
||||||
|
"has_next": page_obj.has_next(),
|
||||||
|
"total": paginator.count,
|
||||||
|
}
|
||||||
|
)
|
|
@ -1,22 +1,18 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from registrar.models import DomainRequest
|
||||||
from registrar.models import DomainRequest, Domain, UserDomainRole
|
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
"""This page is available to anyone without logging in."""
|
"""This page is available to anyone without logging in."""
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
# Get all domain requests the user has access to
|
# Get all domain requests the user has access to
|
||||||
domain_requests, deletable_domain_requests = _get_domain_requests(request)
|
domain_requests, deletable_domain_requests = _get_domain_requests(request)
|
||||||
|
|
||||||
context["domain_requests"] = domain_requests
|
context["domain_requests"] = domain_requests
|
||||||
|
|
||||||
# Get all domains the user has access to
|
|
||||||
domains = _get_domains(request)
|
|
||||||
context["domains"] = domains
|
|
||||||
|
|
||||||
# Determine if the user will see domain requests that they can delete
|
# Determine if the user will see domain requests that they can delete
|
||||||
has_deletable_domain_requests = deletable_domain_requests.exists()
|
has_deletable_domain_requests = deletable_domain_requests.exists()
|
||||||
context["has_deletable_domain_requests"] = has_deletable_domain_requests
|
context["has_deletable_domain_requests"] = has_deletable_domain_requests
|
||||||
|
@ -55,11 +51,3 @@ def _get_domain_requests(request):
|
||||||
deletable_domain_requests = domain_requests.filter(status__in=valid_statuses)
|
deletable_domain_requests = domain_requests.filter(status__in=valid_statuses)
|
||||||
|
|
||||||
return (domain_requests, deletable_domain_requests)
|
return (domain_requests, deletable_domain_requests)
|
||||||
|
|
||||||
|
|
||||||
def _get_domains(request):
|
|
||||||
"""Given the current request,
|
|
||||||
get all domains that are associated with the UserDomainRole object"""
|
|
||||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
|
||||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
|
||||||
return Domain.objects.filter(id__in=domain_ids)
|
|
||||||
|
|
|
@ -2,18 +2,25 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import parse_qs, unquote
|
||||||
|
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from registrar.forms.user_profile import UserProfileForm
|
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
|
||||||
from django.urls import reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Contact,
|
Contact,
|
||||||
)
|
)
|
||||||
|
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 flag_is_active, waffle_flag
|
from waffle.decorators import flag_is_active, waffle_flag
|
||||||
|
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_protect
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -30,13 +37,26 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Handle get requests by getting user's contact object and setting object
|
"""Handle get requests by getting user's contact object and setting object
|
||||||
and form to context before rendering."""
|
and form to context before rendering."""
|
||||||
self.object = self.get_object()
|
self._refresh_session_and_object(request)
|
||||||
form = self.form_class(instance=self.object)
|
form = self.form_class(instance=self.object)
|
||||||
context = self.get_context_data(object=self.object, form=form)
|
context = self.get_context_data(object=self.object, form=form)
|
||||||
|
|
||||||
|
return_to_request = request.GET.get("return_to_request")
|
||||||
|
if return_to_request:
|
||||||
|
context["return_to_request"] = True
|
||||||
|
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
def _refresh_session_and_object(self, request):
|
||||||
|
"""Sets the current session to self.session and the current object to self.object"""
|
||||||
|
self.session = request.session
|
||||||
|
self.object = self.get_object()
|
||||||
|
|
||||||
@waffle_flag("profile_feature") # type: ignore
|
@waffle_flag("profile_feature") # type: ignore
|
||||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||||
|
# Store the original queryparams to persist them
|
||||||
|
query_params = request.META["QUERY_STRING"]
|
||||||
|
request.session["query_params"] = query_params
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -44,15 +64,29 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# This is a django waffle flag which toggles features based off of the "flag" table
|
# This is a django waffle flag which toggles features based off of the "flag" table
|
||||||
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
|
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
|
||||||
|
|
||||||
|
# The text for the back button on this page
|
||||||
|
context["profile_back_button_text"] = "Go to manage your domains"
|
||||||
|
context["show_back_button"] = True
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
"""Redirect to the user's profile page."""
|
"""Redirect to the user's profile page."""
|
||||||
return reverse("user-profile")
|
|
||||||
|
query_params = {}
|
||||||
|
if "query_params" in self.session:
|
||||||
|
params = unquote(self.session["query_params"])
|
||||||
|
query_params = parse_qs(params)
|
||||||
|
|
||||||
|
# Preserve queryparams and add them back to the url
|
||||||
|
base_url = reverse("user-profile")
|
||||||
|
new_redirect = replace_url_queryparams(base_url, query_params, convert_list_to_csv=True)
|
||||||
|
return new_redirect
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Handle post requests (form submissions)"""
|
"""Handle post requests (form submissions)"""
|
||||||
self.object = self.get_object()
|
self._refresh_session_and_object(request)
|
||||||
form = self.form_class(request.POST, instance=self.object)
|
form = self.form_class(request.POST, instance=self.object)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -75,3 +109,145 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
if hasattr(user, "contact"): # Check if the user has a contact instance
|
if hasattr(user, "contact"): # Check if the user has a contact instance
|
||||||
return user.contact
|
return user.contact
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class FinishProfileSetupView(UserProfileView):
|
||||||
|
"""This view forces the user into providing additional details that
|
||||||
|
we may have missed from Login.gov"""
|
||||||
|
|
||||||
|
class RedirectType(Enum):
|
||||||
|
"""
|
||||||
|
Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`.
|
||||||
|
|
||||||
|
- HOME: We want to redirect to reverse("home")
|
||||||
|
- BACK_TO_SELF: We want to redirect back to this page
|
||||||
|
- TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect"
|
||||||
|
- COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent
|
||||||
|
redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE
|
||||||
|
"""
|
||||||
|
|
||||||
|
HOME = "home"
|
||||||
|
TO_SPECIFIC_PAGE = "domain_request"
|
||||||
|
BACK_TO_SELF = "back_to_self"
|
||||||
|
COMPLETE_SETUP = "complete_setup"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_redirect_types(cls) -> list[str]:
|
||||||
|
"""Returns the value of every redirect type defined in this enum."""
|
||||||
|
return [r.value for r in cls]
|
||||||
|
|
||||||
|
template_name = "finish_profile_setup.html"
|
||||||
|
form_class = FinishSetupProfileForm
|
||||||
|
model = Contact
|
||||||
|
|
||||||
|
all_redirect_types = RedirectType.get_all_redirect_types()
|
||||||
|
redirect_type: RedirectType
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# Hide the back button by default
|
||||||
|
context["show_back_button"] = False
|
||||||
|
|
||||||
|
if self.redirect_type == self.RedirectType.COMPLETE_SETUP:
|
||||||
|
context["confirm_changes"] = True
|
||||||
|
|
||||||
|
if "redirect_viewname" not in self.session:
|
||||||
|
context["show_back_button"] = True
|
||||||
|
else:
|
||||||
|
context["going_to_specific_page"] = True
|
||||||
|
context["redirect_button_text"] = "Continue to your request"
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
@method_decorator(csrf_protect)
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag.
|
||||||
|
|
||||||
|
This method sets the redirect type based on the 'redirect' query parameter,
|
||||||
|
defaulting to BACK_TO_SELF if not provided.
|
||||||
|
It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: The response generated by the parent class's dispatch method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Update redirect type based on the query parameter if present
|
||||||
|
default_redirect_value = self.RedirectType.BACK_TO_SELF.value
|
||||||
|
redirect_value = request.GET.get("redirect", default_redirect_value)
|
||||||
|
|
||||||
|
if redirect_value in self.all_redirect_types:
|
||||||
|
# If the redirect value is a preexisting value in our enum, set it to that.
|
||||||
|
self.redirect_type = self.RedirectType(redirect_value)
|
||||||
|
else:
|
||||||
|
# If the redirect type is undefined, then we assume that we are specifying a particular page to redirect to.
|
||||||
|
self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE
|
||||||
|
|
||||||
|
# Store the page that we want to redirect to for later use
|
||||||
|
request.session["redirect_viewname"] = str(redirect_value)
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""Form submission posts to this view."""
|
||||||
|
self._refresh_session_and_object(request)
|
||||||
|
form = self.form_class(request.POST, instance=self.object)
|
||||||
|
|
||||||
|
# Get the current form and validate it
|
||||||
|
if form.is_valid():
|
||||||
|
if "contact_setup_save_button" in request.POST:
|
||||||
|
# Logic for when the 'Save' button is clicked
|
||||||
|
self.redirect_type = self.RedirectType.COMPLETE_SETUP
|
||||||
|
elif "contact_setup_submit_button" in request.POST:
|
||||||
|
specific_redirect = "redirect_viewname" in self.session
|
||||||
|
self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE if specific_redirect else self.RedirectType.HOME
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
"""Redirect to the nameservers page for the domain."""
|
||||||
|
return self.get_redirect_url()
|
||||||
|
|
||||||
|
def get_redirect_url(self):
|
||||||
|
"""
|
||||||
|
Returns a URL string based on the current value of self.redirect_type.
|
||||||
|
|
||||||
|
Depending on self.redirect_type, constructs a base URL and appends a
|
||||||
|
'redirect' query parameter. Handles different redirection types such as
|
||||||
|
HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The full URL with the appropriate query parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# These redirect types redirect to the same page
|
||||||
|
self_redirect = [self.RedirectType.BACK_TO_SELF, self.RedirectType.COMPLETE_SETUP]
|
||||||
|
|
||||||
|
# Maps the redirect type to a URL
|
||||||
|
base_url = ""
|
||||||
|
try:
|
||||||
|
if self.redirect_type in self_redirect:
|
||||||
|
base_url = reverse("finish-user-profile-setup")
|
||||||
|
elif self.redirect_type == self.RedirectType.TO_SPECIFIC_PAGE:
|
||||||
|
# We only allow this session value to use viewnames,
|
||||||
|
# because this restricts what can be redirected to.
|
||||||
|
desired_view = self.session["redirect_viewname"]
|
||||||
|
self.session.pop("redirect_viewname")
|
||||||
|
base_url = reverse(desired_view)
|
||||||
|
else:
|
||||||
|
base_url = reverse("home")
|
||||||
|
except NoReverseMatch as err:
|
||||||
|
logger.error(f"get_redirect_url -> Could not find the specified page. Err: {err}")
|
||||||
|
|
||||||
|
query_params = {}
|
||||||
|
|
||||||
|
# Quote cleans up the value so that it can be used in a url
|
||||||
|
if self.redirect_type and self.redirect_type.value:
|
||||||
|
query_params["redirect"] = quote(self.redirect_type.value)
|
||||||
|
|
||||||
|
# Generate the full url from the given query params
|
||||||
|
full_url = replace_url_queryparams(base_url, query_params)
|
||||||
|
return full_url
|
||||||
|
|
|
@ -296,7 +296,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
||||||
domain_pk = self.kwargs["pk"]
|
domain_pk = self.kwargs["pk"]
|
||||||
user_pk = self.kwargs["user_pk"]
|
user_pk = self.kwargs["user_pk"]
|
||||||
|
|
||||||
# Check if the user is authenticated
|
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -393,6 +392,8 @@ class UserProfilePermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
If the user is authenticated, they have access
|
If the user is authenticated, they have access
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Check if the user is authenticated
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue