mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-26 04:28:39 +02:00
Merge branch 'main' into sspj/domain-interface
This commit is contained in:
commit
5b78b0ec67
31 changed files with 1167 additions and 215 deletions
23
.github/ISSUE_TEMPLATE/bug.yml
vendored
23
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -4,6 +4,12 @@ title: "[Bug]: "
|
|||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
id: help
|
||||
attributes:
|
||||
value: |
|
||||
> **Note**
|
||||
> GitHub Issues use [GitHub Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting.
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
|
@ -24,25 +30,30 @@ body:
|
|||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: "How can the issue be reliably reproduced? feel free to include screenshots or other supporting artifacts"
|
||||
placeholder: |
|
||||
description: |
|
||||
How can the issue be reliably reproduced? Feel free to include screenshots or other supporting artifacts
|
||||
|
||||
Example:
|
||||
1. In the test environment, fill out the application for a new domain
|
||||
2. Click the button to trigger a save/submit on the final page and complete the application
|
||||
3. See the error
|
||||
value: |
|
||||
1.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: "Where is this issue occurring? If related to development environment, list the specific relevant tool versions"
|
||||
placeholder: |
|
||||
label: Environment (optional)
|
||||
description: |
|
||||
Where is this issue occurring? If related to development environment, list the relevant tool versions.
|
||||
|
||||
Example:
|
||||
- Environment: Sandbox
|
||||
- Browser: Chrome x.y.z
|
||||
- Python: x.y.z
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
label: Additional Context (optional)
|
||||
description: "Please include additional references, screenshots, documentation, etc. that are relevant"
|
||||
|
|
62
.github/ISSUE_TEMPLATE/user-story.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/user-story.yml
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
name: User Story
|
||||
description: Capture actionable sprint work
|
||||
title: "[Story]: "
|
||||
labels: ["story"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
id: help
|
||||
attributes:
|
||||
value: |
|
||||
> **Note**
|
||||
> GitHub Issues use [GitHub Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting.
|
||||
- type: textarea
|
||||
id: story
|
||||
attributes:
|
||||
label: User Story
|
||||
description: |
|
||||
Please add the "as a, I want, so that" details that describe the user story.
|
||||
If more than one "as a, I want, so that" describes the story, add multiple.
|
||||
|
||||
Example:
|
||||
As an administrator
|
||||
I want the ability to approve a domain application
|
||||
so that a request can be fulfilled and a new .gov domain can be provisioned
|
||||
value: |
|
||||
As a
|
||||
I want
|
||||
so that
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: acceptance-criteria
|
||||
attributes:
|
||||
label: Acceptance Criteria
|
||||
description: |
|
||||
Please add the acceptance criteria using one or more "given, when, then" formulae
|
||||
|
||||
Example:
|
||||
Given that I am an administrator who has finished reviewing a domain application
|
||||
When I click to approve a domain application
|
||||
Then the domain provisioning process should be initiated, and the applicant should receive an email update.
|
||||
value: |
|
||||
Given
|
||||
When
|
||||
Then
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context (optional)
|
||||
description: "Please include additional references, screenshots, documentation, etc. that are relevant"
|
||||
- type: textarea
|
||||
id: issue-links
|
||||
attributes:
|
||||
label: Issue Links (optional)
|
||||
description: |
|
||||
What other issues does this story relate to and how?
|
||||
|
||||
Example:
|
||||
- 🚧 Blocked by: #123
|
||||
- 🔄 Relates to: #234
|
5
.github/SECURITY.md
vendored
Normal file
5
.github/SECURITY.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
* If you've found a security or privacy issue on the **.gov top-level domain infrastructure**, submit it to our [vulnerabilty disclosure form](https://forms.office.com/Pages/ResponsePage.aspx?id=bOfNPG2UEkq7evydCEI1SqHke9Gh6wJEl3kQ5EjWUKlUMTZZS1lBVkxHUzZURFpLTkE2NEJFVlhVRi4u) or email dotgov@cisa.dhs.gov.
|
||||
* If you see a security or privacy issue on **an individual .gov domain**, check [current-full.csv](https://flatgithub.com/cisagov/dotgov-data/blob/main/?filename=current-full.csv) or [Whois](https://domains.dotgov.gov/dotgov-web/registration/whois.xhtml) (same data) to check whether the domain has a security contact to report your finding directly. You are welcome to Cc dotgov@cisa.dhs.gov on the email.
|
||||
* If you are unable to find a contact or receive no response from the security contact, email dotgov@cisa.dhs.gov.
|
||||
|
||||
Note that most federal (executive branch) agencies maintain a [vulnerability disclosure policy](https://github.com/cisagov/vdp-in-fceb/).
|
4
.github/workflows/reset-db.yaml
vendored
4
.github/workflows/reset-db.yaml
vendored
|
@ -34,8 +34,8 @@ jobs:
|
|||
- name: Delete existing data for ${{ github.event.inputs.environment }}
|
||||
uses: 18f/cg-deploy-action@main
|
||||
with:
|
||||
cf_username: ${{ secrets[CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[CF_PASSWORD] }}
|
||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||
cf_org: cisa-getgov-prototyping
|
||||
cf_space: ${{ github.event.inputs.environment }}
|
||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py flush --no-input' --name flush"
|
||||
|
|
158
docs/architecture/diagrams/model_timeline.md
Normal file
158
docs/architecture/diagrams/model_timeline.md
Normal file
|
@ -0,0 +1,158 @@
|
|||
# Data Model Timeline
|
||||
|
||||
This diagram connects the data models along with various workflow stages.
|
||||
|
||||
1. The applicant starts the process at `/register` interacting with the
|
||||
`DomainApplication` object.
|
||||
|
||||
2. The analyst approves the application using the `DomainApplication`'s
|
||||
`approve()` method which creates many related objects: `UserDomainRole`,
|
||||
`Domain`, and `DomainInformation`.
|
||||
|
||||
3. After the domain is approved, users interact with various
|
||||
`/domain/<id>/...` views which make changes to the `Domain`,
|
||||
`DomainInformation`, and `UserDomainRole` models. For inviting new users,
|
||||
there is a `DomainInvitation` model that allows people to be added to
|
||||
domains who are not already users.
|
||||
|
||||
A more complete diagram of the data models, their fields, and their
|
||||
relationships are in [models_diagram.md](./models_diagram.md), created with
|
||||
the `django-model2puml` plugin.
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary>PlantUML source code</summary>
|
||||
To regenerate this image using Docker, run
|
||||
|
||||
```bash
|
||||
$ docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg model_timeline.md
|
||||
```
|
||||
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
allowmixing
|
||||
left to right direction
|
||||
|
||||
class DomainApplication {
|
||||
Application for a domain
|
||||
--
|
||||
creator (User)
|
||||
investigator (User)
|
||||
authorizing_official (Contact)
|
||||
submitter (Contact)
|
||||
other_contacts (Contacts)
|
||||
approved_domain (Domain)
|
||||
requested_domain (DraftDomain)
|
||||
current_websites (Websites)
|
||||
alternative_domains (Websites)
|
||||
--
|
||||
Request information...
|
||||
}
|
||||
|
||||
class User {
|
||||
Django's user class
|
||||
--
|
||||
...
|
||||
--
|
||||
}
|
||||
note left of User
|
||||
Created by DjangoOIDC
|
||||
when users arrive back
|
||||
from Login.gov
|
||||
|
||||
<b>username</b> is the Login UUID
|
||||
end note
|
||||
|
||||
DomainApplication -l- User : creator, investigator
|
||||
|
||||
class Contact {
|
||||
Contact info for a person
|
||||
--
|
||||
first_name
|
||||
middle_name
|
||||
last_name
|
||||
title
|
||||
email
|
||||
phone
|
||||
--
|
||||
}
|
||||
|
||||
DomainApplication *-r-* Contact : authorizing_official, submitter, other_contacts
|
||||
|
||||
class DraftDomain {
|
||||
Requested domain
|
||||
--
|
||||
name
|
||||
--
|
||||
}
|
||||
|
||||
DomainApplication -l- DraftDomain : requested_domain
|
||||
|
||||
class Domain {
|
||||
Approved domain
|
||||
--
|
||||
name
|
||||
--
|
||||
<b>EPP methods</b>
|
||||
}
|
||||
|
||||
DomainApplication .right[#blue].> Domain : approve()
|
||||
|
||||
class DomainInformation {
|
||||
Registrar information on a domain
|
||||
--
|
||||
domain (Domain)
|
||||
domain_application (DomainApplication)
|
||||
security_email
|
||||
--
|
||||
Request information...
|
||||
}
|
||||
|
||||
DomainInformation -- Domain
|
||||
DomainInformation -- DomainApplication
|
||||
DomainApplication .[#blue].> DomainInformation : approve()
|
||||
|
||||
class UserDomainRole {
|
||||
Permissions
|
||||
--
|
||||
domain (Domain)
|
||||
user (User)
|
||||
role="ADMIN"
|
||||
--
|
||||
}
|
||||
UserDomainRole -- User
|
||||
UserDomainRole -- Domain
|
||||
DomainApplication .[#blue].> UserDomainRole : approve()
|
||||
|
||||
class DomainInvitation {
|
||||
Email invitations sent
|
||||
--
|
||||
email
|
||||
domain (Domain)
|
||||
status
|
||||
--
|
||||
}
|
||||
DomainInvitation -- Domain
|
||||
DomainInvitation .[#green].> UserDomainRole : User.first_login()
|
||||
|
||||
actor applicant #Red
|
||||
applicant -d-> DomainApplication : **/register**
|
||||
|
||||
actor analyst #Blue
|
||||
analyst -[#blue]-> DomainApplication : **approve()**
|
||||
|
||||
actor user1 #Green
|
||||
user1 -[#green]-> Domain : **/domain/<id>/nameservers**
|
||||
actor user2 #Green
|
||||
user2 -[#green]-> DomainInformation : **/domain/<id>/?????**
|
||||
actor user3 #Green
|
||||
user3 -right[#green]-> UserDomainRole : **/domain/<id>/users/add**
|
||||
user3 -right[#green]-> DomainInvitation : **/domain/<id>/users/add**
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
</details>
|
1
docs/architecture/diagrams/model_timeline.svg
Normal file
1
docs/architecture/diagrams/model_timeline.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 30 KiB |
291
docs/architecture/diagrams/models_diagram.md
Normal file
291
docs/architecture/diagrams/models_diagram.md
Normal file
|
@ -0,0 +1,291 @@
|
|||
# Complete model documentation
|
||||
|
||||
This is an auto-generated diagram of our data models generated with the
|
||||
[django-model2puml](https://github.com/sen-den/django-model2puml) library
|
||||
using the command
|
||||
|
||||
```bash
|
||||
$ docker compose app ./manage.py generate_puml --include registrar
|
||||
```
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary>PlantUML source code</summary>
|
||||
|
||||
To regenerate this image using Docker, run
|
||||
|
||||
```bash
|
||||
$ docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md
|
||||
```
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
|
||||
contact
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
~ user (OneToOneField)
|
||||
+ first_name (TextField)
|
||||
+ middle_name (TextField)
|
||||
+ last_name (TextField)
|
||||
+ title (TextField)
|
||||
+ email (TextField)
|
||||
+ phone (PhoneNumberField)
|
||||
--
|
||||
}
|
||||
registrar.Contact -- registrar.User
|
||||
|
||||
|
||||
class "registrar.DomainApplication <Registrar>" as registrar.DomainApplication #d6f4e9 {
|
||||
domain application
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
+ status (FSMField)
|
||||
~ creator (ForeignKey)
|
||||
~ investigator (ForeignKey)
|
||||
+ organization_type (CharField)
|
||||
+ federally_recognized_tribe (BooleanField)
|
||||
+ state_recognized_tribe (BooleanField)
|
||||
+ tribe_name (TextField)
|
||||
+ federal_agency (TextField)
|
||||
+ federal_type (CharField)
|
||||
+ is_election_board (BooleanField)
|
||||
+ organization_name (TextField)
|
||||
+ address_line1 (TextField)
|
||||
+ address_line2 (CharField)
|
||||
+ city (TextField)
|
||||
+ state_territory (CharField)
|
||||
+ zipcode (CharField)
|
||||
+ urbanization (TextField)
|
||||
+ type_of_work (TextField)
|
||||
+ more_organization_information (TextField)
|
||||
~ authorizing_official (ForeignKey)
|
||||
~ approved_domain (OneToOneField)
|
||||
~ requested_domain (OneToOneField)
|
||||
~ submitter (ForeignKey)
|
||||
+ purpose (TextField)
|
||||
+ no_other_contacts_rationale (TextField)
|
||||
+ anything_else (TextField)
|
||||
+ is_policy_acknowledged (BooleanField)
|
||||
# current_websites (ManyToManyField)
|
||||
# alternative_domains (ManyToManyField)
|
||||
# other_contacts (ManyToManyField)
|
||||
--
|
||||
}
|
||||
registrar.DomainApplication -- registrar.User
|
||||
registrar.DomainApplication -- registrar.User
|
||||
registrar.DomainApplication -- registrar.Contact
|
||||
registrar.DomainApplication -- registrar.DraftDomain
|
||||
registrar.DomainApplication -- registrar.Domain
|
||||
registrar.DomainApplication -- registrar.Contact
|
||||
registrar.DomainApplication *--* registrar.Website
|
||||
registrar.DomainApplication *--* registrar.Website
|
||||
registrar.DomainApplication *--* registrar.Contact
|
||||
|
||||
|
||||
class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #d6f4e9 {
|
||||
domain information
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
~ creator (ForeignKey)
|
||||
~ domain_application (OneToOneField)
|
||||
+ organization_type (CharField)
|
||||
+ federally_recognized_tribe (BooleanField)
|
||||
+ state_recognized_tribe (BooleanField)
|
||||
+ tribe_name (TextField)
|
||||
+ federal_agency (TextField)
|
||||
+ federal_type (CharField)
|
||||
+ is_election_board (BooleanField)
|
||||
+ organization_name (TextField)
|
||||
+ address_line1 (TextField)
|
||||
+ address_line2 (CharField)
|
||||
+ city (TextField)
|
||||
+ state_territory (CharField)
|
||||
+ zipcode (CharField)
|
||||
+ urbanization (TextField)
|
||||
+ type_of_work (TextField)
|
||||
+ more_organization_information (TextField)
|
||||
~ authorizing_official (ForeignKey)
|
||||
~ domain (OneToOneField)
|
||||
~ submitter (ForeignKey)
|
||||
+ purpose (TextField)
|
||||
+ no_other_contacts_rationale (TextField)
|
||||
+ anything_else (TextField)
|
||||
+ is_policy_acknowledged (BooleanField)
|
||||
+ security_email (EmailField)
|
||||
# other_contacts (ManyToManyField)
|
||||
--
|
||||
}
|
||||
registrar.DomainInformation -- registrar.User
|
||||
registrar.DomainInformation -- registrar.DomainApplication
|
||||
registrar.DomainInformation -- registrar.Contact
|
||||
registrar.DomainInformation -- registrar.Domain
|
||||
registrar.DomainInformation -- registrar.Contact
|
||||
registrar.DomainInformation *--* registrar.Contact
|
||||
|
||||
|
||||
class "registrar.DraftDomain <Registrar>" as registrar.DraftDomain #d6f4e9 {
|
||||
draft domain
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
+ name (CharField)
|
||||
--
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
user domain role
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
~ user (ForeignKey)
|
||||
~ domain (ForeignKey)
|
||||
+ role (TextField)
|
||||
--
|
||||
}
|
||||
registrar.UserDomainRole -- registrar.User
|
||||
registrar.UserDomainRole -- registrar.Domain
|
||||
|
||||
|
||||
class "registrar.DomainInvitation <Registrar>" as registrar.DomainInvitation #d6f4e9 {
|
||||
domain invitation
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
+ email (EmailField)
|
||||
~ domain (ForeignKey)
|
||||
+ status (FSMField)
|
||||
--
|
||||
}
|
||||
registrar.DomainInvitation -- registrar.Domain
|
||||
|
||||
|
||||
class "registrar.Nameserver <Registrar>" as registrar.Nameserver #d6f4e9 {
|
||||
nameserver
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
+ name (CharField)
|
||||
~ domain (ForeignKey)
|
||||
~ host_ptr (OneToOneField)
|
||||
--
|
||||
}
|
||||
registrar.Nameserver -- registrar.Domain
|
||||
registrar.Nameserver -- 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 (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.PublicContact -- registrar.Domain
|
||||
|
||||
|
||||
class "registrar.User <Registrar>" as registrar.User #d6f4e9 {
|
||||
user
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ password (CharField)
|
||||
+ last_login (DateTimeField)
|
||||
+ is_superuser (BooleanField)
|
||||
+ username (CharField)
|
||||
+ first_name (CharField)
|
||||
+ last_name (CharField)
|
||||
+ email (EmailField)
|
||||
+ is_staff (BooleanField)
|
||||
+ is_active (BooleanField)
|
||||
+ date_joined (DateTimeField)
|
||||
+ phone (PhoneNumberField)
|
||||
# groups (ManyToManyField)
|
||||
# user_permissions (ManyToManyField)
|
||||
# domains (ManyToManyField)
|
||||
--
|
||||
}
|
||||
registrar.User *--* registrar.Domain
|
||||
|
||||
|
||||
class "registrar.Website <Registrar>" as registrar.Website #d6f4e9 {
|
||||
website
|
||||
--
|
||||
+ id (BigAutoField)
|
||||
+ created_at (DateTimeField)
|
||||
+ updated_at (DateTimeField)
|
||||
+ website (CharField)
|
||||
--
|
||||
}
|
||||
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
</details>
|
1
docs/architecture/diagrams/models_diagram.svg
Normal file
1
docs/architecture/diagrams/models_diagram.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 72 KiB |
5
docs/developer/default-WHOIS-contacts.md
Normal file
5
docs/developer/default-WHOIS-contacts.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# How to change the default contact data
|
||||
|
||||
The defaults are located in [src/registrar/models/public_contact.py](../../src/registrar/models/public_contact.py). Change them in the source code and re-deploy.
|
||||
|
||||
The choice of which fields to disclose is hardcoded in [src/registrar/models/domain.py](../../src/registrar/models/domain.py) (May 23) but in the future this may become customizable by registrants.
|
|
@ -37,3 +37,4 @@ django-webtest = "*"
|
|||
types-cachetools = "*"
|
||||
boto3-mocking = "*"
|
||||
boto3-stubs = "*"
|
||||
django-model2puml = "*"
|
||||
|
|
171
src/Pipfile.lock
generated
171
src/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "ebec8b958bcfde525ad74aa1e777b55855e86b2d63264612bc2855bf167070b1"
|
||||
"sha256": "b6c1a957da6c715c734906059a81da21cb0eb4c4ab04f204eb58a48ddb8f7234"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
|
@ -24,19 +24,19 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:38ca632be379963f2a2749b5f63a81fe1679913b954914f470ad282c77674bbc",
|
||||
"sha256:4d575c180312bec6108852bae12e6396b9d1bb404154d652c57ee849c62fbb83"
|
||||
"sha256:62285ecee7629a4388d55ae369536f759622d68d5b9a0ced7c58a0c1a409c0f7",
|
||||
"sha256:8ff0af0b25266a01616396abc19eb34dc3d44bd867fa4158985924128b9034fb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.122"
|
||||
"version": "==1.26.133"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:9e4984a9e9777c6b949aa1e98323fa35480d9f99d447af7e179ae611f7ed5af9",
|
||||
"sha256:c3b41078d235761b9c5dc22f534a76952622ef96787b96bbd10242ec4d73f2a5"
|
||||
"sha256:7b38e540f73c921d8cb0ac72794072000af9e10758c04ba7f53d5629cc52fa87",
|
||||
"sha256:b266185d7414a559952569005009a400de50af91fd3da44f05cf05b00946c4a7"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.29.122"
|
||||
"version": "==1.29.133"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
|
@ -48,11 +48,11 @@
|
|||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
|
||||
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
|
||||
"sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
|
||||
"sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2022.12.7"
|
||||
"version": "==2023.5.7"
|
||||
},
|
||||
"cfenv": {
|
||||
"hashes": [
|
||||
|
@ -261,11 +261,11 @@
|
|||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:ad33ed68db9398f5dfb33282704925bce044bef4261cd4fb59e4e7f9ae505a78",
|
||||
"sha256:c36e2ab12824e2ac36afa8b2515a70c53c7742f0d6eaefa7311ec379558db997"
|
||||
"sha256:066b6debb5ac335458d2a713ed995570536c8b59a580005acb0732378d5eb1ee",
|
||||
"sha256:7efa6b1f781a6119a10ac94b4794ded90db8accbe7802281cd26f8664ffed59c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2"
|
||||
"version": "==4.2.1"
|
||||
},
|
||||
"django-allow-cidr": {
|
||||
"hashes": [
|
||||
|
@ -338,11 +338,11 @@
|
|||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:49060d40e6659e116f53353c5771ad2f2cbcd12b15771f49e3000a3a451f13ec",
|
||||
"sha256:ac903ba8cb5adbce2cdd15e5536118d484bbe01126f3c774dd9f6df77b61232d"
|
||||
"sha256:38dbc3b80e655d7301e190426ab30f04b6b7f6ca4764c5dd02772ffde0fa6dcd",
|
||||
"sha256:f02c6d3fdb5bc781f80b440cf2bdec336ed47ecfb8d620b20c3d4188ed051831"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==18.6.0"
|
||||
"version": "==18.7.0"
|
||||
},
|
||||
"furl": {
|
||||
"hashes": [
|
||||
|
@ -623,19 +623,19 @@
|
|||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b",
|
||||
"sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"
|
||||
"sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294",
|
||||
"sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.29.0"
|
||||
"version": "==2.30.0"
|
||||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
|
||||
"sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
|
||||
"sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
|
||||
"sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.6.0"
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
|
@ -752,11 +752,11 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:38ca632be379963f2a2749b5f63a81fe1679913b954914f470ad282c77674bbc",
|
||||
"sha256:4d575c180312bec6108852bae12e6396b9d1bb404154d652c57ee849c62fbb83"
|
||||
"sha256:62285ecee7629a4388d55ae369536f759622d68d5b9a0ced7c58a0c1a409c0f7",
|
||||
"sha256:8ff0af0b25266a01616396abc19eb34dc3d44bd867fa4158985924128b9034fb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.122"
|
||||
"version": "==1.26.133"
|
||||
},
|
||||
"boto3-mocking": {
|
||||
"hashes": [
|
||||
|
@ -768,27 +768,27 @@
|
|||
},
|
||||
"boto3-stubs": {
|
||||
"hashes": [
|
||||
"sha256:401e7fe51d88a51b527d883d195ed20c7f57aeb2c0aea24bbb3e911b6d2ad3aa",
|
||||
"sha256:743a37bfd7d1eed4d67cdf825283abc1d93b7900b81d7426aab7e691e075c897"
|
||||
"sha256:a921814574761842073822dc5e9fc7ca4f1c5fdeaa53d83cd8831e060dae09c8",
|
||||
"sha256:cc6a662700e755c1e3dec2383c146b89cd8c70b5921033504bfb8367d03a538f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.122"
|
||||
"version": "==1.26.133"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:9e4984a9e9777c6b949aa1e98323fa35480d9f99d447af7e179ae611f7ed5af9",
|
||||
"sha256:c3b41078d235761b9c5dc22f534a76952622ef96787b96bbd10242ec4d73f2a5"
|
||||
"sha256:7b38e540f73c921d8cb0ac72794072000af9e10758c04ba7f53d5629cc52fa87",
|
||||
"sha256:b266185d7414a559952569005009a400de50af91fd3da44f05cf05b00946c4a7"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.29.122"
|
||||
"version": "==1.29.133"
|
||||
},
|
||||
"botocore-stubs": {
|
||||
"hashes": [
|
||||
"sha256:59873a3b535ec3ff0b6bf5f41c9f8a0f8c48032a871bea4d6e4faebbbfc68e8b",
|
||||
"sha256:e6e6c527a6cac0ec69dd1b755d530c9b2dab01d423ce47bdc636dd01ebb01b1b"
|
||||
"sha256:5f6f1967d23c45834858a055cbf65b66863f9f28d05f32f57bf52864a13512d9",
|
||||
"sha256:622c4a5cd740498439008d81c5ded612146f4f0d575341c12591f978edbbe733"
|
||||
],
|
||||
"markers": "python_version >= '3.7' and python_version < '4.0'",
|
||||
"version": "==1.29.122"
|
||||
"version": "==1.29.130"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
|
@ -800,11 +800,11 @@
|
|||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:ad33ed68db9398f5dfb33282704925bce044bef4261cd4fb59e4e7f9ae505a78",
|
||||
"sha256:c36e2ab12824e2ac36afa8b2515a70c53c7742f0d6eaefa7311ec379558db997"
|
||||
"sha256:066b6debb5ac335458d2a713ed995570536c8b59a580005acb0732378d5eb1ee",
|
||||
"sha256:7efa6b1f781a6119a10ac94b4794ded90db8accbe7802281cd26f8664ffed59c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2"
|
||||
"version": "==4.2.1"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
|
@ -814,6 +814,13 @@
|
|||
"index": "pypi",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"django-model2puml": {
|
||||
"hashes": [
|
||||
"sha256:6e773d742e556020a04d3216ce5dee5d3551da162e2d42a997f85b4ed1854771"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"django-stubs": {
|
||||
"hashes": [
|
||||
"sha256:93baff824f0a056e71036b423b942a74f07b909e45e3fa38185b910f597c5c08",
|
||||
|
@ -896,35 +903,35 @@
|
|||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:023fe9e618182ca6317ae89833ba422c411469156b690fde6a315ad10695a521",
|
||||
"sha256:031fc69c9a7e12bcc5660b74122ed84b3f1c505e762cc4296884096c6d8ee140",
|
||||
"sha256:2de7babe398cb7a85ac7f1fd5c42f396c215ab3eff731b4d761d68d0f6a80f48",
|
||||
"sha256:2e93a8a553e0394b26c4ca683923b85a69f7ccdc0139e6acd1354cc884fe0128",
|
||||
"sha256:390bc685ec209ada4e9d35068ac6988c60160b2b703072d2850457b62499e336",
|
||||
"sha256:3a2d219775a120581a0ae8ca392b31f238d452729adbcb6892fa89688cb8306a",
|
||||
"sha256:3efde4af6f2d3ccf58ae825495dbb8d74abd6d176ee686ce2ab19bd025273f41",
|
||||
"sha256:4a99fe1768925e4a139aace8f3fb66db3576ee1c30b9c0f70f744ead7e329c9f",
|
||||
"sha256:4b41412df69ec06ab141808d12e0bf2823717b1c363bd77b4c0820feaa37249e",
|
||||
"sha256:4c8d8c6b80aa4a1689f2a179d31d86ae1367ea4a12855cc13aa3ba24bb36b2d8",
|
||||
"sha256:4d19f1a239d59f10fdc31263d48b7937c585810288376671eaf75380b074f238",
|
||||
"sha256:4e4a682b3f2489d218751981639cffc4e281d548f9d517addfd5a2917ac78119",
|
||||
"sha256:695c45cea7e8abb6f088a34a6034b1d273122e5530aeebb9c09626cea6dca4cb",
|
||||
"sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d",
|
||||
"sha256:70894c5345bea98321a2fe84df35f43ee7bb0feec117a71420c60459fc3e1eed",
|
||||
"sha256:8293a216e902ac12779eb7a08f2bc39ec6c878d7c6025aa59464e0c4c16f7eb9",
|
||||
"sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e",
|
||||
"sha256:a197ad3a774f8e74f21e428f0de7f60ad26a8d23437b69638aac2764d1e06a6a",
|
||||
"sha256:bea55fc25b96c53affab852ad94bf111a3083bc1d8b0c76a61dd101d8a388cf5",
|
||||
"sha256:c9a084bce1061e55cdc0493a2ad890375af359c766b8ac311ac8120d3a472950",
|
||||
"sha256:d0e9464a0af6715852267bf29c9553e4555b61f5904a4fc538547a4d67617937",
|
||||
"sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394",
|
||||
"sha256:eaeaa0888b7f3ccb7bcd40b50497ca30923dba14f385bde4af78fac713d6d6f6",
|
||||
"sha256:f46af8d162f3d470d8ffc997aaf7a269996d205f9d746124a179d3abe05ac602",
|
||||
"sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1",
|
||||
"sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"
|
||||
"sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703",
|
||||
"sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf",
|
||||
"sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4",
|
||||
"sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85",
|
||||
"sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd",
|
||||
"sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae",
|
||||
"sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd",
|
||||
"sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca",
|
||||
"sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305",
|
||||
"sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409",
|
||||
"sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c",
|
||||
"sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb",
|
||||
"sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee",
|
||||
"sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a",
|
||||
"sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228",
|
||||
"sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897",
|
||||
"sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d",
|
||||
"sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f",
|
||||
"sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152",
|
||||
"sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf",
|
||||
"sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8",
|
||||
"sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11",
|
||||
"sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017",
|
||||
"sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929",
|
||||
"sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e",
|
||||
"sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.0"
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
|
@ -968,11 +975,11 @@
|
|||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4",
|
||||
"sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"
|
||||
"sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f",
|
||||
"sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.5.0"
|
||||
"version": "==3.5.1"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
|
@ -1062,11 +1069,11 @@
|
|||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
|
||||
"sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
|
||||
"sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
|
||||
"sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.6.0"
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
|
@ -1118,11 +1125,11 @@
|
|||
},
|
||||
"types-awscrt": {
|
||||
"hashes": [
|
||||
"sha256:40854d9d7ce055620d5d41e5adc84df11b879aedbd2cf20de84e73f084aa5797",
|
||||
"sha256:fe38c6fd71199a9f739b69a7c2f3a574585457c4f63730a62830628a7bffc5b0"
|
||||
"sha256:9e447df3ad46767887d14fa9c856df94f80e8a0a7f0169577ab23b52ee37bcdf",
|
||||
"sha256:e28fb3f20568ce9e96e33e01e0b87b891822f36b8f368adb582553b016d4aa08"
|
||||
],
|
||||
"markers": "python_version >= '3.7' and python_version < '4.0'",
|
||||
"version": "==0.16.16"
|
||||
"version": "==0.16.17"
|
||||
},
|
||||
"types-cachetools": {
|
||||
"hashes": [
|
||||
|
@ -1148,26 +1155,26 @@
|
|||
},
|
||||
"types-requests": {
|
||||
"hashes": [
|
||||
"sha256:0d580652ce903f643f8c3b494dd01d29367ea57cea0c7ad7f65cf3169092edb0",
|
||||
"sha256:cc1aba862575019306b2ed134eb1ea994cab1c887a22e18d3383e6dd42e9789b"
|
||||
"sha256:c6cf08e120ca9f0dc4fa4e32c3f953c3fba222bcc1db6b97695bce8da1ba9864",
|
||||
"sha256:dec781054324a70ba64430ae9e62e7e9c8e4618c185a5cb3f87a6738251b5a31"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.28.11.17"
|
||||
"version": "==2.30.0.0"
|
||||
},
|
||||
"types-s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:40e665643f0647832d51c4a26d8a8275cda9134b02bf22caf28198b79bcad382",
|
||||
"sha256:d9c669b30fdd61347720434aacb8ecc4645d900712a70b10f495104f9039c07b"
|
||||
"sha256:6d1ac1dedac750d570428362acdf60fdd4f277b0788855c3894d3226756b2bfb",
|
||||
"sha256:75ac1d7143d58c1e6af467cfd4a96c67ee058a3adf7c249d9309999e1f5f41e4"
|
||||
],
|
||||
"markers": "python_version >= '3.7' and python_version < '4.0'",
|
||||
"version": "==0.6.0.post7"
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"types-urllib3": {
|
||||
"hashes": [
|
||||
"sha256:04235e792139cf3624b25d38faab593456738fbdb7439634046172e3b1339400",
|
||||
"sha256:697102ddf4f781eed6f692353f40cee1098643526f5a8b99f49d2ede90fd3754"
|
||||
"sha256:3300538c9dc11dad32eae4827ac313f5d986b8b21494801f1bf97a1ac6c03ae5",
|
||||
"sha256:5dbd1d2bef14efee43f5318b5d36d805a489f6600252bb53626d4bfafd95e27c"
|
||||
],
|
||||
"version": "==1.26.25.11"
|
||||
"version": "==1.26.25.13"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
|
|
|
@ -43,10 +43,15 @@ except NameError:
|
|||
# Attn: these imports should NOT be at the top of the file
|
||||
try:
|
||||
from .client import CLIENT, commands
|
||||
from .errors import RegistryError, ErrorCode
|
||||
from epplib.models import common
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__all__ = [
|
||||
"CLIENT",
|
||||
"commands",
|
||||
"common",
|
||||
"ErrorCode",
|
||||
"RegistryError",
|
||||
]
|
||||
|
|
|
@ -67,54 +67,47 @@ class EPPLibWrapper:
|
|||
def _send(self, command):
|
||||
"""Helper function used by `send`."""
|
||||
try:
|
||||
cmd_type = command.__class__.__name__
|
||||
with self._connect as wire:
|
||||
response = wire.send(command)
|
||||
except (ValueError, ParsingError) as err:
|
||||
logger.warning(
|
||||
"%s failed to execute due to some syntax error."
|
||||
% command.__class__.__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
raise RegistryError() from err
|
||||
message = "%s failed to execute due to some syntax error."
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
except TransportError as err:
|
||||
logger.warning(
|
||||
"%s failed to execute due to a connection error."
|
||||
% command.__class__.__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
raise RegistryError() from err
|
||||
message = "%s failed to execute due to a connection error."
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
except LoginError as err:
|
||||
logger.warning(
|
||||
"%s failed to execute due to a registry login error."
|
||||
% command.__class__.__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
raise RegistryError() from err
|
||||
message = "%s failed to execute due to a registry login error."
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
except Exception as err:
|
||||
logger.warning(
|
||||
"%s failed to execute due to an unknown error."
|
||||
% command.__class__.__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
raise RegistryError() from err
|
||||
message = "%s failed to execute due to an unknown error."
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
else:
|
||||
if response.code >= 2000:
|
||||
raise RegistryError(response.msg)
|
||||
raise RegistryError(response.msg, code=response.code)
|
||||
else:
|
||||
return response
|
||||
|
||||
def send(self, command):
|
||||
def send(self, command, *, cleaned=False):
|
||||
"""Login, send the command, then close the connection. Tries 3 times."""
|
||||
# try to prevent use of this method without appropriate safeguards
|
||||
if not cleaned:
|
||||
raise ValueError("Please sanitize user input before sending it.")
|
||||
|
||||
counter = 0 # we'll try 3 times
|
||||
while True:
|
||||
try:
|
||||
return self._send(command)
|
||||
except RegistryError as err:
|
||||
if counter == 3: # don't try again
|
||||
raise err
|
||||
else:
|
||||
if err.should_retry() and counter < 3:
|
||||
counter += 1
|
||||
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms
|
||||
else: # don't try again
|
||||
raise err
|
||||
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,5 +1,77 @@
|
|||
from enum import IntEnum
|
||||
|
||||
|
||||
class ErrorCode(IntEnum):
|
||||
"""
|
||||
Overview of registry response codes from RFC 5730. See RFC 5730 for full text.
|
||||
|
||||
- 1000 - 1500 Success
|
||||
- 2000 - 2308 Registrar did something silly
|
||||
- 2400 - 2500 Registry did something silly
|
||||
- 2501 - 2502 Something malicious or abusive may have occurred
|
||||
"""
|
||||
|
||||
COMMAND_COMPLETED_SUCCESSFULLY = 1000
|
||||
COMMAND_COMPLETED_SUCCESSFULLY_ACTION_PENDING = 1001
|
||||
COMMAND_COMPLETED_SUCCESSFULLY_NO_MESSAGES = 1300
|
||||
COMMAND_COMPLETED_SUCCESSFULLY_ACK_TO_DEQUEUE = 1301
|
||||
COMMAND_COMPLETED_SUCCESSFULLY_ENDING_SESSION = 1500
|
||||
|
||||
UNKNOWN_COMMAND = 2000
|
||||
COMMAND_SYNTAX_ERROR = 2001
|
||||
COMMAND_USE_ERROR = 2002
|
||||
REQUIRED_PARAMETER_MISSING = 2003
|
||||
PARAMETER_VALUE_RANGE_ERROR = 2004
|
||||
PARAMETER_VALUE_SYNTAX_ERROR = 2005
|
||||
UNIMPLEMENTED_PROTOCOL_VERSION = 2100
|
||||
UNIMPLEMENTED_COMMAND = 2101
|
||||
UNIMPLEMENTED_OPTION = 2102
|
||||
UNIMPLEMENTED_EXTENSION = 2103
|
||||
BILLING_FAILURE = 2104
|
||||
OBJECT_IS_NOT_ELIGIBLE_FOR_RENEWAL = 2105
|
||||
OBJECT_IS_NOT_ELIGIBLE_FOR_TRANSFER = 2106
|
||||
AUTHENTICATION_ERROR = 2200
|
||||
AUTHORIZATION_ERROR = 2201
|
||||
INVALID_AUTHORIZATION_INFORMATION = 2202
|
||||
OBJECT_PENDING_TRANSFER = 2300
|
||||
OBJECT_NOT_PENDING_TRANSFER = 2301
|
||||
OBJECT_EXISTS = 2302
|
||||
OBJECT_DOES_NOT_EXIST = 2303
|
||||
OBJECT_STATUS_PROHIBITS_OPERATION = 2304
|
||||
OBJECT_ASSOCIATION_PROHIBITS_OPERATION = 2305
|
||||
PARAMETER_VALUE_POLICY_ERROR = 2306
|
||||
UNIMPLEMENTED_OBJECT_SERVICE = 2307
|
||||
DATA_MANAGEMENT_POLICY_VIOLATION = 2308
|
||||
|
||||
COMMAND_FAILED = 2400
|
||||
COMMAND_FAILED_SERVER_CLOSING_CONNECTION = 2500
|
||||
|
||||
AUTHENTICATION_ERROR_SERVER_CLOSING_CONNECTION = 2501
|
||||
SESSION_LIMIT_EXCEEDED_SERVER_CLOSING_CONNECTION = 2502
|
||||
|
||||
|
||||
class RegistryError(Exception):
|
||||
pass
|
||||
"""
|
||||
Overview of registry response codes from RFC 5730. See RFC 5730 for full text.
|
||||
|
||||
- 1000 - 1500 Success
|
||||
- 2000 - 2308 Registrar did something silly
|
||||
- 2400 - 2500 Registry did something silly
|
||||
- 2501 - 2502 Something malicious or abusive may have occurred
|
||||
"""
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code = code
|
||||
|
||||
def should_retry(self):
|
||||
return self.code == ErrorCode.COMMAND_FAILED
|
||||
|
||||
def is_server_error(self):
|
||||
return self.code is not None and (self.code >= 2400 and self.code <= 2500)
|
||||
|
||||
def is_client_error(self):
|
||||
return self.code is not None and (self.code >= 2000 and self.code <= 2308)
|
||||
|
||||
|
||||
class LoginError(RegistryError):
|
||||
|
|
|
@ -109,6 +109,8 @@ INSTALLED_APPS = [
|
|||
"registrar",
|
||||
# Our internal API application
|
||||
"api",
|
||||
# Only for generating documentation, uncomment to run manage.py generate_puml
|
||||
# "puml_generator",
|
||||
]
|
||||
|
||||
# Middleware are routines for processing web requests.
|
||||
|
|
|
@ -59,12 +59,12 @@ urlpatterns = [
|
|||
),
|
||||
path(
|
||||
"application/<int:pk>/withdraw",
|
||||
views.ApplicationWithdraw.as_view(),
|
||||
views.ApplicationWithdrawConfirmation.as_view(),
|
||||
name="application-withdraw-confirmation",
|
||||
),
|
||||
path(
|
||||
"application/<int:pk>/withdrawconfirmed",
|
||||
views.ApplicationWithdraw.updatestatus,
|
||||
views.ApplicationWithdrawn.as_view(),
|
||||
name="application-withdrawn",
|
||||
),
|
||||
path("health/", views.health),
|
||||
|
@ -83,6 +83,11 @@ urlpatterns = [
|
|||
views.DomainNameserversView.as_view(),
|
||||
name="domain-nameservers",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/your-contact-information",
|
||||
views.DomainYourContactInformationView.as_view(),
|
||||
name="domain-your-contact-information",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/security-email",
|
||||
views.DomainSecurityEmailView.as_view(),
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
from .application_wizard import *
|
||||
from .domain import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm
|
||||
from .domain import (
|
||||
DomainAddUserForm,
|
||||
NameserverFormset,
|
||||
DomainSecurityEmailForm,
|
||||
ContactForm,
|
||||
)
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
from django import forms
|
||||
from django.forms import formset_factory
|
||||
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
||||
from ..models import Contact
|
||||
|
||||
|
||||
class DomainAddUserForm(forms.Form):
|
||||
|
||||
|
@ -29,3 +33,34 @@ class DomainSecurityEmailForm(forms.Form):
|
|||
"""Form for adding or editing a security email to a domain."""
|
||||
|
||||
security_email = forms.EmailField(label="Security email")
|
||||
|
||||
|
||||
class ContactForm(forms.ModelForm):
|
||||
|
||||
"""Form for updating contacts."""
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||
widgets = {
|
||||
"first_name": forms.TextInput,
|
||||
"middle_name": forms.TextInput,
|
||||
"last_name": forms.TextInput,
|
||||
"title": forms.TextInput,
|
||||
"email": forms.EmailInput,
|
||||
"phone": RegionalPhoneNumberWidget,
|
||||
}
|
||||
|
||||
# the database fields have blank=True so ModelForm doesn't create
|
||||
# required fields by default. Use this list in __init__ to mark each
|
||||
# of these fields as required
|
||||
required = ["first_name", "last_name", "title", "email", "phone"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# take off maxlength attribute for the phone number field
|
||||
# which interferes with our input_with_errors template tag
|
||||
self.fields["phone"].widget.attrs.pop("maxlength", None)
|
||||
|
||||
for field_name in self.required:
|
||||
self.fields[field_name].required = True
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# Generated by Django 4.2.1 on 2023-05-31 23:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0022_draftdomain_domainapplication_approved_domain_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="first_name",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text="First name",
|
||||
null=True,
|
||||
verbose_name="first name / given name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="last_name",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text="Last name",
|
||||
null=True,
|
||||
verbose_name="last name / family name",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="title",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="Title",
|
||||
null=True,
|
||||
verbose_name="title or role in your organization",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -20,6 +20,7 @@ class Contact(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
help_text="First name",
|
||||
verbose_name="first name / given name",
|
||||
db_index=True,
|
||||
)
|
||||
middle_name = models.TextField(
|
||||
|
@ -31,12 +32,14 @@ class Contact(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
help_text="Last name",
|
||||
verbose_name="last name / family name",
|
||||
db_index=True,
|
||||
)
|
||||
title = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Title",
|
||||
verbose_name="title or role in your organization",
|
||||
)
|
||||
email = models.TextField(
|
||||
null=True,
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<p> <b class="review__step__name">Last updated:</b> {{domainapplication.updated_at|date:"F j, Y"}}<br>
|
||||
<b class="review__step__name">Request #:</b> {{domainapplication.id}}</p>
|
||||
<p>{% include "includes/domain_application.html" %}</p>
|
||||
<p><a href="{% url 'application-withdraw-confirmation' domainapplication.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
<p><a href="{% url 'application-withdraw-confirmation' pk=domainapplication.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'todo' as url %}
|
||||
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
|
35
src/registrar/templates/domain_your_contact_information.html
Normal file
35
src/registrar/templates/domain_your_contact_information.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers %}
|
||||
|
||||
{% block title %}Domain contact information | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
|
||||
<h1>Domain contact information</h1>
|
||||
|
||||
<p>If you’d like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here won’t affect your Login.gov account information.</p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
|
||||
{% input_with_errors form.first_name %}
|
||||
|
||||
{% input_with_errors form.middle_name %}
|
||||
|
||||
{% input_with_errors form.last_name %}
|
||||
|
||||
{% input_with_errors form.title %}
|
||||
|
||||
{% input_with_errors form.email %}
|
||||
|
||||
{% input_with_errors form.phone %}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save</button>
|
||||
</form>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
|
@ -13,6 +13,7 @@ import boto3_mocking # type: ignore
|
|||
from registrar.models import (
|
||||
DomainApplication,
|
||||
Domain,
|
||||
DomainInformation,
|
||||
DraftDomain,
|
||||
DomainInvitation,
|
||||
Contact,
|
||||
|
@ -1030,12 +1031,16 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain
|
||||
)
|
||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.domain_information.delete()
|
||||
if hasattr(self.domain, "contacts"):
|
||||
self.domain.contacts.all().delete()
|
||||
self.domain.delete()
|
||||
|
@ -1048,55 +1053,39 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
class TestDomainPermissions(TestWithDomainPermissions):
|
||||
def test_not_logged_in(self):
|
||||
"""Not logged in gets a redirect to Login."""
|
||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("domain-security-email", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
for view_name in [
|
||||
"domain",
|
||||
"domain-users",
|
||||
"domain-users-add",
|
||||
"domain-nameservers",
|
||||
"domain-your-contact-information",
|
||||
"domain-security-email",
|
||||
]:
|
||||
with self.subTest(view_name=view_name):
|
||||
response = self.client.get(
|
||||
reverse(view_name, kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_no_domain_role(self):
|
||||
"""Logged in but no role gets 403 Forbidden."""
|
||||
self.client.force_login(self.user)
|
||||
self.role.delete() # user no longer has a role on this domain
|
||||
|
||||
with less_console_noise():
|
||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
with less_console_noise():
|
||||
response = self.client.get(
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
with less_console_noise():
|
||||
response = self.client.get(
|
||||
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
with less_console_noise():
|
||||
response = self.client.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
for view_name in [
|
||||
"domain",
|
||||
"domain-users",
|
||||
"domain-users-add",
|
||||
"domain-nameservers",
|
||||
"domain-your-contact-information",
|
||||
"domain-security-email",
|
||||
]:
|
||||
with self.subTest(view_name=view_name):
|
||||
with less_console_noise():
|
||||
response = self.client.get(
|
||||
reverse(view_name, kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
with less_console_noise():
|
||||
response = self.client.get(
|
||||
|
@ -1139,7 +1128,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
self.assertContains(response, "Add another user")
|
||||
|
||||
def test_domain_user_add_form(self):
|
||||
"""Adding a user works."""
|
||||
"""Adding an existing user works."""
|
||||
other_user, _ = get_user_model().objects.get_or_create(
|
||||
email="mayor@igorville.gov"
|
||||
)
|
||||
|
@ -1222,6 +1211,22 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
with self.assertRaises(DomainInvitation.DoesNotExist):
|
||||
DomainInvitation.objects.get(id=invitation.id)
|
||||
|
||||
def test_domain_invitation_cancel_no_permissions(self):
|
||||
"""Posting to the delete view as a different user should fail."""
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
invitation, _ = DomainInvitation.objects.get_or_create(
|
||||
domain=self.domain, email=EMAIL
|
||||
)
|
||||
|
||||
other_user = User()
|
||||
other_user.save()
|
||||
self.client.force_login(other_user)
|
||||
with less_console_noise(): # permission denied makes console errors
|
||||
result = self.client.post(
|
||||
reverse("invitation-delete", kwargs={"pk": invitation.id})
|
||||
)
|
||||
self.assertEqual(result.status_code, 403)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_domain_invitation_flow(self):
|
||||
"""Send an invitation to a new user, log in and load the dashboard."""
|
||||
|
@ -1296,6 +1301,23 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
|||
# the field.
|
||||
self.assertContains(result, "This field is required", count=2, status_code=200)
|
||||
|
||||
def test_domain_your_contact_information(self):
|
||||
"""Can load domain's your contact information page."""
|
||||
page = self.client.get(
|
||||
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(page, "Domain contact information")
|
||||
|
||||
def test_domain_your_contact_information_content(self):
|
||||
"""Your contact information appears on the page."""
|
||||
self.domain_information.submitter = Contact(first_name="Testy")
|
||||
self.domain_information.submitter.save()
|
||||
self.domain_information.save()
|
||||
page = self.app.get(
|
||||
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(page, "Testy")
|
||||
|
||||
def test_domain_security_email(self):
|
||||
"""Can load domain's security email page."""
|
||||
page = self.client.get(
|
||||
|
@ -1333,6 +1355,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self.app.set_user(self.user.username)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def _completed_application(
|
||||
self,
|
||||
|
@ -1446,3 +1469,24 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
)
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "Withdrawn")
|
||||
|
||||
def test_application_status_no_permissions(self):
|
||||
"""Can't access applications without being the creator."""
|
||||
application = self._completed_application()
|
||||
other_user = User()
|
||||
other_user.save()
|
||||
application.creator = other_user
|
||||
application.save()
|
||||
|
||||
# PermissionDeniedErrors make lots of noise in test output
|
||||
with less_console_noise():
|
||||
for url_name in [
|
||||
"application-status",
|
||||
"application-withdraw-confirmation",
|
||||
"application-withdrawn",
|
||||
]:
|
||||
with self.subTest(url_name=url_name):
|
||||
page = self.client.get(
|
||||
reverse(url_name, kwargs={"pk": application.pk})
|
||||
)
|
||||
self.assertEqual(page.status_code, 403)
|
||||
|
|
|
@ -2,6 +2,7 @@ from .application import *
|
|||
from .domain import (
|
||||
DomainView,
|
||||
DomainNameserversView,
|
||||
DomainYourContactInformationView,
|
||||
DomainSecurityEmailView,
|
||||
DomainUsersView,
|
||||
DomainAddUserView,
|
||||
|
|
|
@ -6,7 +6,6 @@ from django.shortcuts import redirect, render
|
|||
from django.urls import resolve, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
from django.views import generic
|
||||
from django.contrib import messages
|
||||
|
||||
from registrar.forms import application_wizard as forms
|
||||
|
@ -14,7 +13,7 @@ from registrar.models import DomainApplication
|
|||
from registrar.utility import StrEnum
|
||||
from registrar.views.utility import StepsHelper
|
||||
|
||||
from .utility import DomainPermission
|
||||
from .utility import DomainApplicationPermissionView
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -478,29 +477,31 @@ class Finished(ApplicationWizard):
|
|||
return render(self.request, self.template_name, context)
|
||||
|
||||
|
||||
class ApplicationStatus(generic.DetailView):
|
||||
model = DomainApplication
|
||||
class ApplicationStatus(DomainApplicationPermissionView):
|
||||
template_name = "application_status.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Get context details to process information from application"""
|
||||
context = super(ApplicationStatus, self).get_context_data(**kwargs)
|
||||
return context
|
||||
|
||||
class ApplicationWithdrawConfirmation(DomainApplicationPermissionView):
|
||||
"""This page will ask user to confirm if they want to withdraw
|
||||
|
||||
class ApplicationWithdraw(LoginRequiredMixin, generic.DetailView, DomainPermission):
|
||||
model = DomainApplication
|
||||
template_name = "application_withdraw_confirmation.html"
|
||||
""" The page above will display asking user to confirm if they want to withdraw;
|
||||
|
||||
Note it uses "DomainPermission" from Domain to ensure that the person who
|
||||
applied only have access to withdraw the request
|
||||
The DomainApplicationPermissionView restricts access so that only the
|
||||
`creator` of the application may withdraw it.
|
||||
"""
|
||||
|
||||
def updatestatus(request, pk):
|
||||
"""If user click on withdraw confirm button, it will be updated to withdraw
|
||||
and send back to homepage"""
|
||||
application = DomainApplication.objects.get(id=pk)
|
||||
template_name = "application_withdraw_confirmation.html"
|
||||
|
||||
|
||||
class ApplicationWithdrawn(DomainApplicationPermissionView):
|
||||
# this view renders no template
|
||||
template_name = ""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""View class that does the actual withdrawing.
|
||||
|
||||
If user click on withdraw confirm button, this view updates the status
|
||||
to withdraw and send back to homepage.
|
||||
"""
|
||||
application = DomainApplication.objects.get(id=self.kwargs["pk"])
|
||||
application.status = "withdrawn"
|
||||
application.save()
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
"""View for a single Domain."""
|
||||
"""Views for a single Domain.
|
||||
|
||||
Authorization is handled by the `DomainPermissionView`. To ensure that only
|
||||
authorized users can see information on a domain, every view here should
|
||||
inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView).
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -7,35 +12,39 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||
from django.db import IntegrityError
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.views.generic import DetailView
|
||||
from django.views.generic.edit import DeleteView, FormMixin
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
from registrar.models import Domain, DomainInvitation, User, UserDomainRole
|
||||
from registrar.models import (
|
||||
DomainInvitation,
|
||||
User,
|
||||
UserDomainRole,
|
||||
)
|
||||
|
||||
from ..forms import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm
|
||||
from ..forms import (
|
||||
DomainAddUserForm,
|
||||
NameserverFormset,
|
||||
DomainSecurityEmailForm,
|
||||
ContactForm,
|
||||
)
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
from .utility import DomainPermission
|
||||
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainView(DomainPermission, DetailView):
|
||||
class DomainView(DomainPermissionView):
|
||||
|
||||
"""Domain detail overview page."""
|
||||
|
||||
model = Domain
|
||||
template_name = "domain_detail.html"
|
||||
context_object_name = "domain"
|
||||
|
||||
|
||||
class DomainNameserversView(DomainPermission, FormMixin, DetailView):
|
||||
class DomainNameserversView(DomainPermissionView, FormMixin):
|
||||
|
||||
"""Domain nameserver editing view."""
|
||||
|
||||
model = Domain
|
||||
template_name = "domain_nameservers.html"
|
||||
context_object_name = "domain"
|
||||
form_class = NameserverFormset
|
||||
|
||||
def get_initial(self):
|
||||
|
@ -44,7 +53,7 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView):
|
|||
return [{"server": name} for name, *ip in domain.nameservers]
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
"""Redirect to the nameservers page for the domain."""
|
||||
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -97,13 +106,51 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView):
|
|||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView):
|
||||
class DomainYourContactInformationView(DomainPermissionView, FormMixin):
|
||||
|
||||
"""Domain your contact information editing view."""
|
||||
|
||||
template_name = "domain_your_contact_information.html"
|
||||
form_class = ContactForm
|
||||
|
||||
def get_form_kwargs(self, *args, **kwargs):
|
||||
"""Add domain_info.submitter instance to make a bound form."""
|
||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
form_kwargs["instance"] = self.get_object().domain_info.submitter
|
||||
return form_kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the your contact information for the domain."""
|
||||
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
# there is a valid email address in the form
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, call setter in model."""
|
||||
|
||||
# Post to DB using values from the form
|
||||
form.save()
|
||||
|
||||
messages.success(
|
||||
self.request, "Your contact information for this domain has been updated."
|
||||
)
|
||||
# superclass has the redirect
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
||||
|
||||
"""Domain security email editing view."""
|
||||
|
||||
model = Domain
|
||||
template_name = "domain_security_email.html"
|
||||
context_object_name = "domain"
|
||||
form_class = DomainSecurityEmailForm
|
||||
|
||||
def get_initial(self):
|
||||
|
@ -114,11 +161,11 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView):
|
|||
return initial
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
"""Redirect to the security email page for the domain."""
|
||||
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Formset submission posts to this view."""
|
||||
"""Form submission posts to this view."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
|
@ -144,16 +191,14 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView):
|
|||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
class DomainUsersView(DomainPermission, DetailView):
|
||||
class DomainUsersView(DomainPermissionView):
|
||||
|
||||
"""User management page in the domain details."""
|
||||
|
||||
model = Domain
|
||||
template_name = "domain_users.html"
|
||||
context_object_name = "domain"
|
||||
|
||||
|
||||
class DomainAddUserView(DomainPermission, FormMixin, DetailView):
|
||||
class DomainAddUserView(DomainPermissionView, FormMixin):
|
||||
|
||||
"""Inside of a domain's user management, a form for adding users.
|
||||
|
||||
|
@ -162,7 +207,6 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView):
|
|||
"""
|
||||
|
||||
template_name = "domain_add_user.html"
|
||||
model = Domain
|
||||
form_class = DomainAddUserForm
|
||||
|
||||
def get_success_url(self):
|
||||
|
@ -242,8 +286,9 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView):
|
|||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
class DomainInvitationDeleteView(SuccessMessageMixin, DeleteView):
|
||||
model = DomainInvitation
|
||||
class DomainInvitationDeleteView(
|
||||
DomainInvitationPermissionDeleteView, SuccessMessageMixin
|
||||
):
|
||||
object: DomainInvitation # workaround for type mismatch in DeleteView
|
||||
|
||||
def get_success_url(self):
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
from .steps_helper import StepsHelper
|
||||
from .always_404 import always_404
|
||||
from .mixins import DomainPermission
|
||||
|
||||
from .permission_views import (
|
||||
DomainPermissionView,
|
||||
DomainApplicationPermissionView,
|
||||
DomainInvitationPermissionDeleteView,
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
|
||||
from registrar.models import UserDomainRole
|
||||
from registrar.models import UserDomainRole, DomainApplication, DomainInvitation
|
||||
|
||||
|
||||
class PermissionsLoginMixin(PermissionRequiredMixin):
|
||||
|
@ -35,3 +35,48 @@ class DomainPermission(PermissionsLoginMixin):
|
|||
|
||||
# if we need to check more about the nature of role, do it here.
|
||||
return True
|
||||
|
||||
|
||||
class DomainApplicationPermission(PermissionsLoginMixin):
|
||||
|
||||
"""Does the logged-in user have access to this domain application?"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain application.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# user needs to be the creator of the application
|
||||
# this query is empty if there isn't a domain application with this
|
||||
# id and this user as creator
|
||||
if not DomainApplication.objects.filter(
|
||||
creator=self.request.user, id=self.kwargs["pk"]
|
||||
).exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DomainInvitationPermission(PermissionsLoginMixin):
|
||||
|
||||
"""Does the logged-in user have access to this domain invitation?
|
||||
|
||||
A user has access to a domain invitation if they have a role on the
|
||||
associated domain.
|
||||
"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has a role on the domain of this invitation."""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if not DomainInvitation.objects.filter(
|
||||
id=self.kwargs["pk"], domain__permissions__user=self.request.user
|
||||
).exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
69
src/registrar/views/utility/permission_views.py
Normal file
69
src/registrar/views/utility/permission_views.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
"""View classes that enforce authorization."""
|
||||
|
||||
import abc # abstract base class
|
||||
|
||||
from django.views.generic import DetailView, DeleteView
|
||||
|
||||
from registrar.models import Domain, DomainApplication, DomainInvitation
|
||||
|
||||
from .mixins import (
|
||||
DomainPermission,
|
||||
DomainApplicationPermission,
|
||||
DomainInvitationPermission,
|
||||
)
|
||||
|
||||
|
||||
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||
|
||||
"""Abstract base view for domains that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = Domain
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "domain"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, abc.ABC):
|
||||
|
||||
"""Abstract base view for domain applications that enforces permissions
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = DomainApplication
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "domainapplication"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DomainInvitationPermissionDeleteView(
|
||||
DomainInvitationPermission, DeleteView, abc.ABC
|
||||
):
|
||||
|
||||
"""Abstract view for deleting a domain invitation.
|
||||
|
||||
This one is fairly specialized, but this is the only thing that we do
|
||||
right now with domain invitations. We still have the full
|
||||
`DomainInvitationPermission` class, but here we just pair it with a
|
||||
DeleteView.
|
||||
"""
|
||||
|
||||
model = DomainInvitation
|
||||
object: DomainInvitation # workaround for type mismatch in DeleteView
|
|
@ -52,6 +52,7 @@
|
|||
10038 OUTOFSCOPE http://app:8080/users
|
||||
10038 OUTOFSCOPE http://app:8080/users/add
|
||||
10038 OUTOFSCOPE http://app:8080/nameservers
|
||||
10038 OUTOFSCOPE http://app:8080/your-contact-information
|
||||
10038 OUTOFSCOPE http://app:8080/security-email
|
||||
10038 OUTOFSCOPE http://app:8080/delete
|
||||
10038 OUTOFSCOPE http://app:8080/withdraw
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue