mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-04 00:42:16 +02:00
Merge remote-tracking branch 'origin' into ab/requirements-update
This commit is contained in:
commit
eb5d39b8fd
97 changed files with 2989 additions and 433 deletions
33
.github/workflows/daily-csv-upload.yaml
vendored
Normal file
33
.github/workflows/daily-csv-upload.yaml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
name: Upload current-full.csv and current-federal.csv
|
||||
run-name: Upload current-full.csv and current-federal.csv
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Runs every day at 5 AM UTC.
|
||||
- cron: "0 5 * * *"
|
||||
|
||||
jobs:
|
||||
upload-reports:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CF_USERNAME: CF_${{ secrets.CF_REPORT_ENV }}_USERNAME
|
||||
CF_PASSWORD: CF_${{ secrets.CF_REPORT_ENV }}_PASSWORD
|
||||
steps:
|
||||
- name: Generate current-federal.csv
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ secrets.CF_REPORT_ENV }}
|
||||
cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_federal_report' --name federal"
|
||||
|
||||
- name: Generate current-full.csv
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ secrets.CF_REPORT_ENV }}
|
||||
cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_full_report' --name full"
|
||||
|
1
.github/workflows/deploy-development.yaml
vendored
1
.github/workflows/deploy-development.yaml
vendored
|
@ -15,7 +15,6 @@ on:
|
|||
|
||||
jobs:
|
||||
deploy-development:
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
@ -295,7 +295,7 @@ sudo sntp -sS time.nist.gov
|
|||
```
|
||||
|
||||
## Connection pool
|
||||
To handle our connection to the registry, we utilize a connection pool to keep a socket open to increase responsiveness. In order to accomplish this, we are utilizing a heavily modified version of the (geventconnpool)[https://github.com/rasky/geventconnpool] library.
|
||||
To handle our connection to the registry, we utilize a connection pool to keep a socket open to increase responsiveness. In order to accomplish this, we are utilizing a heavily modified version of the [geventconnpool](https://github.com/rasky/geventconnpool) library.
|
||||
|
||||
### Settings
|
||||
The config for the connection pool exists inside the `settings.py` file.
|
||||
|
@ -319,4 +319,36 @@ Our connection pool has a built-in `pool_status` object which you can call at an
|
|||
5. `print(registry.pool_status.connection_success)`
|
||||
* Should return true
|
||||
|
||||
If you have multiple instances (staging for example), then repeat commands 1-5 for each instance you want to test.
|
||||
If you have multiple instances (staging for example), then repeat commands 1-5 for each instance you want to test.
|
||||
|
||||
## 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.
|
||||
|
||||
To associate a S3 instance to your sandbox, follow these steps:
|
||||
1. Navigate to https://dashboard.fr.cloud.gov/login
|
||||
2. Select your sandbox from the `Applications` tab
|
||||
3. Click `Services` on the application nav bar
|
||||
4. Add a new service (plus symbol)
|
||||
5. Click `Marketplace Service`
|
||||
6. On the `Select the service` dropdown, select `s3`
|
||||
7. Under the dropdown on `Select Plan`, select `basic-sandbox`
|
||||
8. Under `Service Instance` enter `getgov-s3` for the name
|
||||
|
||||
See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI.
|
||||
|
||||
### Testing your S3 instance locally
|
||||
To test the S3 bucket associated with your sandbox, you will need to add four additional variables to your `.env` file. These are as follows:
|
||||
|
||||
```
|
||||
AWS_S3_ACCESS_KEY_ID = "{string value of `access_key_id` in getgov-s3}"
|
||||
AWS_S3_SECRET_ACCESS_KEY = "{string value of `secret_access_key` in getgov-s3}"
|
||||
AWS_S3_REGION = "{string value of `region` in getgov-s3}"
|
||||
AWS_S3_BUCKET_NAME = "{string value of `bucket` in getgov-s3}"
|
||||
```
|
||||
|
||||
You can view these variables by running the following command:
|
||||
```
|
||||
cf env getgov-{app name}
|
||||
```
|
||||
|
||||
Then, copy the variables under the section labled `s3`.
|
86
docs/developer/generating-emails-guide.md
Normal file
86
docs/developer/generating-emails-guide.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
**Below is a step by step guide for generating emails (of each type) in our application. These instructions are UI facing, with the assumption that the user has analyst (or above) permissions.**
|
||||
|
||||
## Domain Invitation
|
||||
- Starting Location: Home page
|
||||
- Workflow: (Domains Table) Manage domain
|
||||
- Workflow Step: Click "Manage" -> Click "Domain managers" -> Click "Add a domain manager" -> (enter email) -> Click "Add user"
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_invitation.txt)
|
||||
|
||||
### Domain Invitation Subject
|
||||
- Notes: Subject line of the "Domain Invitation" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_invitation_subject.txt)
|
||||
|
||||
## Domain Request Withdrawn
|
||||
- Starting Location: Home page
|
||||
- Workflow: (Domain requests Table) Manage domain
|
||||
- Workflow Step: Click "Manage" -> Click "Withdraw request" -> (confirmation prompt) -> Click "Withdraw request" (inside prompt)
|
||||
- Notes: You can also do this through Django Admin by switching a domain of status "submitted" to "withdrawn", but you need to be the submitter (email listed on Your Contact Information).
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn.txt)
|
||||
|
||||
### Domain Request Withdrawn Subject
|
||||
- Notes: Subject line of the "Domain Request Withdrawn" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn_subject.txt)
|
||||
|
||||
## Status Change Action Needed
|
||||
- Starting Location: Django Admin
|
||||
- Workflow: Analyst Admin
|
||||
- Workflow Step: Click "Domain applications" -> Click an application with a status of "in review" or "rejected" -> Click status dropdown -> (select "action needed") -> click "Save"
|
||||
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, set the status to either "in review" or "rejected" (and click save), then set the status to "action needed". This will send you an email.
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_action_needed.txt)
|
||||
|
||||
### Status Change Action Needed Subject
|
||||
- Notes: Subject line of the "Status Change Action Needed" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_action_needed_subject.txt)
|
||||
|
||||
## Status Change in Review
|
||||
- Starting Location: Django Admin
|
||||
- Workflow: Analyst Admin
|
||||
- Workflow Step: Click "Domain applications" -> Click an application with a status of "submitted" -> Click status dropdown -> (select "In review") -> click "Save"
|
||||
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, then set the status to "In review". This will send you an email.
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_approved.txt)
|
||||
|
||||
### Status Change in Review Subject
|
||||
- Notes: This is the subject line of the "Status Change In Review" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_in_review_subject.txt)
|
||||
|
||||
## Status Change Approved
|
||||
- Starting Location: Django Admin
|
||||
- Workflow: Analyst Admin
|
||||
- Workflow Step: Click "Domain applications" -> Click an application in a status of "submitted", "In review", "rejected", or "ineligible" -> Click status dropdown -> (select "approved") -> click "Save"
|
||||
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, then set the status to "approved". This will send you an email.
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_approved.txt)
|
||||
|
||||
### Status Change Approved Subject
|
||||
- Notes: This is the subject line of the "Status Change Approved" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_approved_subject.txt)
|
||||
|
||||
## Status Change Rejected
|
||||
- Starting Location: Django Admin
|
||||
- Workflow: Analyst Admin
|
||||
- Workflow Step: Click "Domain applications" -> Click an application in a status of "In review", or "approved" -> Click status dropdown -> (select "rejected") -> click "Save"
|
||||
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, then set the status to "in review" (and click save). Then, go back to the same application and set the status to "rejected". This will send you an email.
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_rejected.txt)
|
||||
|
||||
### Status Change Rejected Subject
|
||||
- Notes: Subject line of the "Status Change Rejected" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_rejected_subject.txt)
|
||||
|
||||
## Submission Confirmation
|
||||
- Starting Location: Home Page
|
||||
- Workflow: Start domain request
|
||||
- Workflow Step: Click "Start a new domain request" -> (fill out the form) -> On the last step ("Review and submit your domain request "), click "Submit your domain request"
|
||||
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information)
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/submission_confirmation.txt)
|
||||
|
||||
### Submission Confirmation Subject
|
||||
- Notes: This is the subject line of the "Submission Confirmation Subject" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/submission_confirmation_subject.txt)
|
||||
|
||||
## Transition Domain Invitation
|
||||
- Notes: This email is generated during the migration process, meaning that there is no using-facing method to receive this email. This email will be sent out when the following conditions are true: a) The domain exists in the data that Verisign sent us, b) the transition domain script ran successfully, and c) invitations are sent out
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/transition_domain_invitation.txt)
|
||||
|
||||
### Transition Domain Invitation Subject
|
||||
- Notes: This is the subject line of the "Transition Domain Invitation" email
|
||||
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/transition_domain_invitation_subject.txt)
|
||||
|
123
docs/developer/migration-troubleshooting.md
Normal file
123
docs/developer/migration-troubleshooting.md
Normal file
|
@ -0,0 +1,123 @@
|
|||
## Troubleshooting
|
||||
|
||||
### Your toolkit
|
||||
For a general overview, read [this documentation](https://www.algotech.solutions/blog/python/django-migrations-and-how-to-manage-conflicts/)
|
||||
|
||||
|
||||
Some common commands:
|
||||
- docker-compose exec app bash -- gets you into bash
|
||||
- ./manage.py showmigrations -- shows the current migrations that are finished, all should have [x]
|
||||
- ./manage.py makemigrations -- makes the migration
|
||||
- ./manage.py showmigrations -- now you should see the new/updated migration with a [ ]
|
||||
- ./manage.py migrate [folder name here, ie registrar] -- applies those changes to db for specific folder
|
||||
- ./manage.py showmigrations -- the migration changes should now have a [x] by it
|
||||
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Scenario 1: Conflicting migrations on local
|
||||
|
||||
If you get conflicting migrations on local, you probably have a new migration on your branch and you merged main which had new migrations as well. Do NOT merge migrations together.
|
||||
|
||||
Assuming your local migration is `40_local_migration` and the migration from main is `40_some_migration_from_main`:
|
||||
- Delete `40_local_migration`
|
||||
- Run `docker-compose exec app ./manage.py makemigrations`
|
||||
- Run `docker-compose down`
|
||||
- Run `docker-compose up`
|
||||
- Run `docker-compose exec app ./manage.py migrate`
|
||||
|
||||
You should end up with `40_some_migration_from_main`, `41_local_migration`
|
||||
|
||||
Alternatively, assuming that the conflicting migrations are not dependent on each other, you can manually edit the migration file such that your new migration is incremented by one (file name, and definition inside the file) but this approach is not recommended.
|
||||
|
||||
### Scenario 2: Conflicting migrations on sandbox
|
||||
|
||||
This occurs when the logs return the following:
|
||||
>Conflicting migrations detected; multiple leaf nodes in the migration graph: (0040_example, 0041_example in base).
|
||||
To fix them run 'python manage.py makemigrations --merge'
|
||||
|
||||
This happens when you swap branches on your sandbox that contain diverging leaves (eg: 0040_example, 0041_example). The fix is to go into the sandbox, delete one of these leaves, fake run the preceding migration, hand run the remaining previously conflicting leaf, fake run the last migration:
|
||||
|
||||
- `cf login -a api.fr.cloud.gov --sso`
|
||||
- `cf ssh getgov-<app>`
|
||||
- `/tmp/lifecycle/shell`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 39_previous_miration --fake' --name migrate`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 41_example_migration' --name migrate`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 45_last_migration --fake' --name migrate`
|
||||
|
||||
Then, navigate to and delete the offending migration. In this case, it is 0041_example_migration.
|
||||
|
||||
### Scenario 3: Migrations ran incorrectly, and migrate no longer works (sandbox)
|
||||
|
||||
This has happened when updating user perms (so running a new data migration). Something is off with the update on the sandbox and you need to run that last data migration again:
|
||||
- `cf login -a api.fr.cloud.gov --sso`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate registrar 39_penultimate_miration --fake' --name migrate`
|
||||
- `cf run-task getgov-<app> --wait --command 'python manage.py migrate' --name migrate`
|
||||
|
||||
### Scenario 4: All migrations refuse to load due to existing duplicates on sandboxes
|
||||
|
||||
This typically happens with a DB conflict that prevents 001_initial from loading. For instance, let's say all migrations have ran successfully before, and a zero command is ran to reset everything. This can lead to a catastrophic issue with your postgres database.
|
||||
|
||||
To diagnose this issue, you will have to manually delete tables using the psql shell environment. If you are in a production environment and cannot lose that data, then you will need some method of backing that up and reattaching it to the table.
|
||||
|
||||
1. `cf login -a api.fr.cloud.gov --sso`
|
||||
2. Run `cf connect-to-service -no-client getgov-{environment_name} getgov-{environment_name}-database` to open a SSH tunnel
|
||||
3. Run `psql -h localhost -p {port} -U {username} -d {broker_name}`
|
||||
4. Open a new terminal window and run `cf ssh getgov{environment_name}`
|
||||
5. Within that window, run `tmp/lifecycle/shell`
|
||||
6. Within that window, run `./manage.py migrate` and observe which tables are duplicates
|
||||
|
||||
Afterwards, go back to your psql instance. Run the following for each problematic table:
|
||||
|
||||
7. `DROP TABLE {table_name} CASCADE`
|
||||
|
||||
**WARNING:** this will permanently erase data! Be careful when doing this and exercise common sense.
|
||||
|
||||
Then, run `./manage.py migrate` again and repeat step 7 for each table which returns this error.
|
||||
After these errors are resolved, follow instructions in the other scenarios if applicable.
|
||||
|
||||
### Scenario 5: Permissions group exist, but my users cannot log onto the sandbox
|
||||
|
||||
This is most likely due to fixtures not running or fixtures running before the data creating migration. Simple run fixtures again (WARNING: This applies to dev sandboxes only. We never want to rerun fixtures on a stable environment)
|
||||
|
||||
- `cf login -a api.fr.cloud.gov --sso`
|
||||
- `cf run-task getgov-<app> --command "./manage.py load" --name fixtures`
|
||||
|
||||
### Scenario 6: Data is corrupted on the sandbox
|
||||
|
||||
Example: there are extra columns created on a table by an old migration long since gone from the code. In that case, you may have to tunnel into your DB on the sandbox and hand-delete these columns. See scenario #4 if you are running into duplicate table definitions. Also see [this documentation](docs/developer/database-access.md) for a good reference here:
|
||||
|
||||
- `cf login -a api.fr.cloud.gov --sso`
|
||||
- Open a new terminal window and run `cf ssh getgov{environment_name}`
|
||||
- Run `tmp/lifecycle/shell`
|
||||
- Run `./manage.py migrate` and observe which tables have invalid column definitions
|
||||
- Run the `\l` command to see all of the databases that are present
|
||||
- `\c cgawsbrokerprodlgi635s6c0afp8w` (assume cgawsbrokerprodlgi635s6c0afp8w is your DB)
|
||||
‘\dt’ to see the tables
|
||||
- `SELECT * FROM {bad_table};`
|
||||
- `alter table registrar_domain drop {bad_column};`
|
||||
|
||||
### Scenario 7: Continual 500 error for the registrar + your requests (login, clicking around, etc) are not showing up in the logstream
|
||||
|
||||
Example: You are able to log in and access the /admin page, but when you arrive at the registrar you keep getting 500 errors and your log-ins any API calls you make via the UI does not show up in the log stream. And you feel like you’re starting to lose your marbles.
|
||||
|
||||
In the CLI, run the command `cf routes`
|
||||
If you notice that your route of `getgov-<app>.app.cloud.gov` is pointing two apps, then that is probably the major issue of the 500 error. (ie mine was pointing at `getgov-<app>.app.cloud.gov` AND `cisa-dotgov`
|
||||
In the CLI, run the command `cf apps` to check that it has an app running called `cisa-dotgov`. If so, there’s the error!
|
||||
Essentially this shows that your requests were being handled by two completely separate applications and that’s why some requests aren’t being located.
|
||||
To resolve this issue, remove the app named `cisa-dotgov` from this space.
|
||||
Test out the sandbox from there and it should be working!
|
||||
|
||||
**Debug connectivity**
|
||||
|
||||
dig https://getgov-<app>.app.cloud.gov (domain information groper, gets DNS nameserver information)
|
||||
curl -v https://getgov-<app>.app.cloud.gov/ --resolve 'getgov-<app>.app.cloud.gov:<your-ip-address-from-dig-command-above-here>' (this gets you access to ping to it)
|
||||
You should be able to play around with your sandbox and see from the curl command above that it’s being pinged. This command is basically log stream, but gives you full access to make sure you can ping the sandbox manually
|
||||
https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1697810600723069
|
||||
|
||||
### Scenario 8: Can’t log into sandbox, permissions do not exist
|
||||
|
||||
- Fake migrate the migration that’s before the last data creation migration
|
||||
- Run the last data creation migration (AND ONLY THAT ONE)
|
||||
- Fake migrate the last migration in the migration list
|
||||
- Rerun fixtures
|
|
@ -15,7 +15,7 @@ from epplibwrapper import (
|
|||
commands,
|
||||
)
|
||||
|
||||
API_BASE_PATH = "/api/v1/available/"
|
||||
API_BASE_PATH = "/api/v1/available/?domain="
|
||||
|
||||
|
||||
class AvailableViewTest(MockEppLib):
|
||||
|
@ -69,8 +69,8 @@ class AvailableViewTest(MockEppLib):
|
|||
self.assertTrue(check_domain_available("igorville.gov"))
|
||||
# input is lowercased so GSA.GOV should also not be available
|
||||
self.assertFalse(check_domain_available("GSA.gov"))
|
||||
# input is lowercased so IGORVILLE.GOV should also not be available
|
||||
self.assertFalse(check_domain_available("IGORVILLE.gov"))
|
||||
# input is lowercased so IGORVILLE.GOV should also be available
|
||||
self.assertTrue(check_domain_available("IGORVILLE.gov"))
|
||||
|
||||
def test_domain_available_dotgov(self):
|
||||
"""Domain searches work without trailing .gov"""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Internal API views"""
|
||||
from django.apps import apps
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import JsonResponse
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from registrar.templatetags.url_helpers import public_site_url
|
||||
|
@ -13,6 +13,8 @@ from login_required import login_not_required
|
|||
|
||||
from cachetools.func import ttl_cache
|
||||
|
||||
from registrar.utility.s3_bucket import S3ClientError, S3ClientHelper
|
||||
|
||||
|
||||
DOMAIN_FILE_URL = "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
||||
|
||||
|
@ -30,7 +32,9 @@ DOMAIN_API_MESSAGES = {
|
|||
"Read more about choosing your .gov domain.</a>".format(public_site_url("domains/choosing"))
|
||||
),
|
||||
"invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
|
||||
"success": "That domain is available!",
|
||||
"success": "That domain is available! We’ll try to give you the domain you want, \
|
||||
but it's not guaranteed. After you complete this form, we’ll \
|
||||
evaluate whether your request meets our requirements.",
|
||||
"error": GenericError.get_error_message(GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
|
||||
}
|
||||
|
||||
|
@ -82,6 +86,7 @@ def available(request, domain=""):
|
|||
Response is a JSON dictionary with the key "available" and value true or
|
||||
false.
|
||||
"""
|
||||
domain = request.GET.get("domain", "")
|
||||
DraftDomain = apps.get_model("registrar.DraftDomain")
|
||||
# validate that the given domain could be a domain name and fail early if
|
||||
# not.
|
||||
|
@ -95,3 +100,36 @@ def available(request, domain=""):
|
|||
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]})
|
||||
except Exception:
|
||||
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["error"]})
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@login_not_required
|
||||
def get_current_full(request, file_name="current-full.csv"):
|
||||
"""This will return the file content of current-full.csv which is the command
|
||||
output of generate_current_full_report.py. This command iterates through each Domain
|
||||
and returns a CSV representation."""
|
||||
return serve_file(file_name)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@login_not_required
|
||||
def get_current_federal(request, file_name="current-federal.csv"):
|
||||
"""This will return the file content of current-federal.csv which is the command
|
||||
output of generate_current_federal_report.py. This command iterates through each Domain
|
||||
and returns a CSV representation."""
|
||||
return serve_file(file_name)
|
||||
|
||||
|
||||
def serve_file(file_name):
|
||||
"""Downloads a file based on a given filepath. Returns a 500 if not found."""
|
||||
s3_client = S3ClientHelper()
|
||||
# Serve the CSV file. If not found, an exception will be thrown.
|
||||
# This will then be caught by flat, causing it to not read it - which is what we want.
|
||||
try:
|
||||
file = s3_client.get_file(file_name, decode_to_utf=True)
|
||||
except S3ClientError as err:
|
||||
# TODO - #1317: Notify operations when auto report generation fails
|
||||
raise err
|
||||
|
||||
response = HttpResponse(file)
|
||||
return response
|
||||
|
|
|
@ -100,7 +100,9 @@ class Client(oic.Client):
|
|||
"state": session["state"],
|
||||
"nonce": session["nonce"],
|
||||
"redirect_uri": self.registration_response["redirect_uris"][0],
|
||||
"acr_values": self.behaviour.get("acr_value"),
|
||||
# acr_value may be passed in session if overriding, as in the case
|
||||
# of step up auth, otherwise get from settings.py
|
||||
"acr_values": session.get("acr_value") or self.behaviour.get("acr_value"),
|
||||
}
|
||||
|
||||
if extra_args is not None:
|
||||
|
@ -162,7 +164,6 @@ class Client(oic.Client):
|
|||
logger.error(err)
|
||||
logger.error("Unable to parse response for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
# ErrorResponse is not raised, it is passed back...
|
||||
if isinstance(authn_response, ErrorResponse):
|
||||
error = authn_response.get("error", "")
|
||||
|
@ -207,7 +208,6 @@ class Client(oic.Client):
|
|||
logger.error(err)
|
||||
logger.error("Unable to request user info for %s" % state)
|
||||
raise o_e.AuthenticationFailed(locator=state)
|
||||
|
||||
# ErrorResponse is not raised, it is passed back...
|
||||
if isinstance(info_response, ErrorResponse):
|
||||
logger.error("Unable to get user info (%s) for %s" % (info_response.get("error", ""), state))
|
||||
|
@ -272,6 +272,11 @@ class Client(oic.Client):
|
|||
|
||||
super(Client, self).store_response(resp, info)
|
||||
|
||||
def get_step_up_acr_value(self):
|
||||
"""returns the step_up_acr_value from settings
|
||||
this helper function is called from djangooidc views"""
|
||||
return self.behaviour.get("step_up_acr_value")
|
||||
|
||||
def __repr__(self):
|
||||
return "Client {} {} {}".format(
|
||||
self.client_id,
|
||||
|
|
30
src/djangooidc/tests/test_oidc.py
Normal file
30
src/djangooidc/tests/test_oidc.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
import logging
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from djangooidc.oidc import Client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OidcTest(TestCase):
|
||||
def test_oidc_create_authn_request_with_acr_value(self):
|
||||
"""Test that create_authn_request returns a redirect with an acr_value
|
||||
when an acr_value is passed through session.
|
||||
|
||||
This test is only valid locally. On local, client can be initialized.
|
||||
Client initialization does not work in pipeline, so test is useless in
|
||||
pipeline. However, it will not fail in pipeline."""
|
||||
try:
|
||||
# Initialize provider using pyOICD
|
||||
OP = getattr(settings, "OIDC_ACTIVE_PROVIDER")
|
||||
CLIENT = Client(OP)
|
||||
session = {"acr_value": "some_acr_value_maybe_ial2"}
|
||||
response = CLIENT.create_authn_request(session)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("some_acr_value_maybe_ial2", response.url)
|
||||
except Exception as err:
|
||||
logger.warning(err)
|
||||
logger.warning("Unable to configure OpenID Connect provider in pipeline. Cannot execute this test.")
|
|
@ -1,8 +1,9 @@
|
|||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.test import Client, TestCase
|
||||
from django.test import Client, TestCase, RequestFactory
|
||||
from django.urls import reverse
|
||||
from ..views import login_callback
|
||||
|
||||
from .common import less_console_noise
|
||||
|
||||
|
@ -11,6 +12,7 @@ from .common import less_console_noise
|
|||
class ViewsTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def say_hi(*args):
|
||||
return HttpResponse("Hi")
|
||||
|
@ -59,19 +61,83 @@ class ViewsTest(TestCase):
|
|||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
# test
|
||||
with less_console_noise():
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("logout"))
|
||||
|
||||
def test_login_callback_no_step_up_auth(self, mock_client):
|
||||
"""Walk through login_callback when requires_step_up_auth returns False
|
||||
and assert that we have a redirect to /"""
|
||||
# setup
|
||||
session = self.client.session
|
||||
session.save()
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
# test
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/")
|
||||
|
||||
def test_requires_step_up_auth(self, mock_client):
|
||||
"""Invoke login_callback passing it a request when requires_step_up_auth returns True
|
||||
and assert that session is updated and create_authn_request (mock) is called."""
|
||||
# Configure the mock to return an expected value for get_step_up_acr_value
|
||||
mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value"
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.get("/some-url")
|
||||
request.session = {"acr_value": ""}
|
||||
|
||||
# Ensure that the CLIENT instance used in login_callback is the mock
|
||||
# patch requires_step_up_auth to return True
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch(
|
||||
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
|
||||
) as mock_create_authn_request:
|
||||
login_callback(request)
|
||||
|
||||
# create_authn_request only gets called when requires_step_up_auth is True
|
||||
# and it changes this acr_value in request.session
|
||||
|
||||
# Assert that acr_value is no longer empty string
|
||||
self.assertNotEqual(request.session["acr_value"], "")
|
||||
# And create_authn_request was called again
|
||||
mock_create_authn_request.assert_called_once()
|
||||
|
||||
def test_does_not_requires_step_up_auth(self, mock_client):
|
||||
"""Invoke login_callback passing it a request when requires_step_up_auth returns False
|
||||
and assert that session is not updated and create_authn_request (mock) is not called.
|
||||
|
||||
Possibly redundant with test_login_callback_requires_step_up_auth"""
|
||||
# Create a mock request
|
||||
request = self.factory.get("/some-url")
|
||||
request.session = {"acr_value": ""}
|
||||
|
||||
# Ensure that the CLIENT instance used in login_callback is the mock
|
||||
# patch requires_step_up_auth to return False
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch(
|
||||
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
|
||||
) as mock_create_authn_request:
|
||||
login_callback(request)
|
||||
|
||||
# create_authn_request only gets called when requires_step_up_auth is True
|
||||
# and it changes this acr_value in request.session
|
||||
|
||||
# Assert that acr_value is NOT updated by testing that it is still an empty string
|
||||
self.assertEqual(request.session["acr_value"], "")
|
||||
# Assert create_authn_request was not called
|
||||
mock_create_authn_request.assert_not_called()
|
||||
|
||||
@patch("djangooidc.views.authenticate")
|
||||
def test_login_callback_raises(self, mock_auth, mock_client):
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
mock_auth.return_value = None
|
||||
# test
|
||||
with less_console_noise():
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
|
|
@ -11,7 +11,7 @@ from urllib.parse import parse_qs, urlencode
|
|||
|
||||
from djangooidc.oidc import Client
|
||||
from djangooidc import exceptions as o_e
|
||||
|
||||
from registrar.models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -68,6 +68,12 @@ def login_callback(request):
|
|||
try:
|
||||
query = parse_qs(request.GET.urlencode())
|
||||
userinfo = CLIENT.callback(query, request.session)
|
||||
# test for need for identity verification and if it is satisfied
|
||||
# if not satisfied, redirect user to login with stepped up acr_value
|
||||
if requires_step_up_auth(userinfo):
|
||||
# add acr_value to request.session
|
||||
request.session["acr_value"] = CLIENT.get_step_up_acr_value()
|
||||
return CLIENT.create_authn_request(request.session)
|
||||
user = authenticate(request=request, **userinfo)
|
||||
if user:
|
||||
login(request, user)
|
||||
|
@ -79,10 +85,27 @@ def login_callback(request):
|
|||
return error_page(request, err)
|
||||
|
||||
|
||||
def requires_step_up_auth(userinfo):
|
||||
"""if User.needs_identity_verification and step_up_acr_value not in
|
||||
ial returned from callback, return True"""
|
||||
step_up_acr_value = CLIENT.get_step_up_acr_value()
|
||||
acr_value = userinfo.get("ial", "")
|
||||
uuid = userinfo.get("sub", "")
|
||||
email = userinfo.get("email", "")
|
||||
if acr_value != step_up_acr_value:
|
||||
# The acr of this attempt is not at the highest level
|
||||
# so check if the user needs the higher level
|
||||
return User.needs_identity_verification(email, uuid)
|
||||
else:
|
||||
# This attempt already came back at the highest level
|
||||
# so does not require step up
|
||||
return False
|
||||
|
||||
|
||||
def logout(request, next_page=None):
|
||||
"""Redirect the user to the authentication provider (OP) logout page."""
|
||||
try:
|
||||
username = request.user.username
|
||||
user = request.user
|
||||
request_args = {
|
||||
"client_id": CLIENT.client_id,
|
||||
"state": request.session["state"],
|
||||
|
@ -94,7 +117,6 @@ def logout(request, next_page=None):
|
|||
request_args.update(
|
||||
{"post_logout_redirect_uri": CLIENT.registration_response["post_logout_redirect_uris"][0]}
|
||||
)
|
||||
|
||||
url = CLIENT.provider_info["end_session_endpoint"]
|
||||
url += "?" + urlencode(request_args)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -104,7 +126,7 @@ def logout(request, next_page=None):
|
|||
# Always remove Django session stuff - even if not logged out from OP.
|
||||
# Don't wait for the callback as it may never come.
|
||||
auth_logout(request)
|
||||
logger.info("Successfully logged out user %s" % username)
|
||||
logger.info("Successfully logged out user %s" % user)
|
||||
next_page = getattr(settings, "LOGOUT_REDIRECT_URL", None)
|
||||
if next_page:
|
||||
request.session["next"] = next_page
|
||||
|
|
|
@ -51,6 +51,11 @@ services:
|
|||
# AWS credentials
|
||||
- AWS_ACCESS_KEY_ID
|
||||
- AWS_SECRET_ACCESS_KEY
|
||||
# AWS S3 bucket credentials
|
||||
- AWS_S3_ACCESS_KEY_ID
|
||||
- AWS_S3_SECRET_ACCESS_KEY
|
||||
- AWS_S3_REGION
|
||||
- AWS_S3_BUCKET_NAME
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
|
|
|
@ -17,7 +17,7 @@ except ImportError:
|
|||
from django.conf import settings
|
||||
|
||||
from .cert import Cert, Key
|
||||
from .errors import LoginError, RegistryError
|
||||
from .errors import ErrorCode, LoginError, RegistryError
|
||||
from .socket import Socket
|
||||
from .utility.pool import EPPConnectionPool
|
||||
|
||||
|
@ -115,7 +115,7 @@ class EPPLibWrapper:
|
|||
except TransportError as err:
|
||||
message = f"{cmd_type} failed to execute due to a connection error."
|
||||
logger.error(f"{message} Error: {err}", exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
raise RegistryError(message, code=ErrorCode.TRANSPORT_ERROR) from err
|
||||
except LoginError as err:
|
||||
# For linter due to it not liking this line length
|
||||
text = "failed to execute due to a registry login error."
|
||||
|
@ -163,7 +163,8 @@ class EPPLibWrapper:
|
|||
try:
|
||||
return self._send(command)
|
||||
except RegistryError as err:
|
||||
if err.should_retry() and counter < 3:
|
||||
if counter < 3 and (err.should_retry() or err.is_transport_error()):
|
||||
logger.info(f"Retrying transport error. Attempt #{counter+1} of 3.")
|
||||
counter += 1
|
||||
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms
|
||||
else: # don't try again
|
||||
|
|
|
@ -4,13 +4,15 @@ from enum import IntEnum
|
|||
class ErrorCode(IntEnum):
|
||||
"""
|
||||
Overview of registry response codes from RFC 5730. See RFC 5730 for full text.
|
||||
|
||||
- 0 System connection error
|
||||
- 1000 - 1500 Success
|
||||
- 2000 - 2308 Registrar did something silly
|
||||
- 2400 - 2500 Registry did something silly
|
||||
- 2501 - 2502 Something malicious or abusive may have occurred
|
||||
"""
|
||||
|
||||
TRANSPORT_ERROR = 0
|
||||
|
||||
COMMAND_COMPLETED_SUCCESSFULLY = 1000
|
||||
COMMAND_COMPLETED_SUCCESSFULLY_ACTION_PENDING = 1001
|
||||
COMMAND_COMPLETED_SUCCESSFULLY_NO_MESSAGES = 1300
|
||||
|
@ -67,6 +69,9 @@ class RegistryError(Exception):
|
|||
def should_retry(self):
|
||||
return self.code == ErrorCode.COMMAND_FAILED
|
||||
|
||||
def is_transport_error(self):
|
||||
return self.code == ErrorCode.TRANSPORT_ERROR
|
||||
|
||||
# connection errors have error code of None and [Errno 99] in the err message
|
||||
def is_connection_error(self):
|
||||
return self.code is None
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
from django import forms
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django_fsm import get_available_FIELD_transitions
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
|
@ -117,6 +118,15 @@ class ListHeaderAdmin(AuditedAdmin):
|
|||
)
|
||||
return filters
|
||||
|
||||
# customize the help_text for all formfields for manytomany
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
formfield.help_text = (
|
||||
formfield.help_text
|
||||
+ " If more than one value is selected, the change/delete/view actions will be disabled."
|
||||
)
|
||||
return formfield
|
||||
|
||||
|
||||
class UserContactInline(admin.StackedInline):
|
||||
"""Edit a user's profile on the user page."""
|
||||
|
@ -351,6 +361,17 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
|
|||
|
||||
autocomplete_fields = ["user", "domain"]
|
||||
|
||||
# Fixes a bug where non-superusers are redirected to the main page
|
||||
def delete_view(self, request, object_id, extra_context=None):
|
||||
"""Custom delete_view implementation that specifies redirect behaviour"""
|
||||
response = super().delete_view(request, object_id, extra_context)
|
||||
|
||||
if isinstance(response, HttpResponseRedirect) and not request.user.has_perm("registrar.full_access_permission"):
|
||||
url = reverse("admin:registrar_userdomainrole_changelist")
|
||||
return redirect(url)
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
class DomainInvitationAdmin(ListHeaderAdmin):
|
||||
"""Custom domain invitation admin class."""
|
||||
|
@ -445,7 +466,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"No other employees from your organization?",
|
||||
{"fields": ["no_other_contacts_rationale"]},
|
||||
),
|
||||
("Anything else we should know?", {"fields": ["anything_else"]}),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating .gov domains",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
|
@ -464,6 +485,17 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"is_policy_acknowledged",
|
||||
]
|
||||
|
||||
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
||||
# to activate the edit/delete/view buttons
|
||||
filter_horizontal = ("other_contacts",)
|
||||
|
||||
# lists in filter_horizontal are not sorted properly, sort them
|
||||
# by first_name
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
if db_field.name in ("other_contacts",):
|
||||
kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 1 conditions that determine which fields are read-only:
|
||||
|
@ -495,14 +527,14 @@ class DomainApplicationAdminForm(forms.ModelForm):
|
|||
current_state = application.status
|
||||
|
||||
# first option in status transitions is current state
|
||||
available_transitions = [(current_state, current_state)]
|
||||
available_transitions = [(current_state, application.get_status_display())]
|
||||
|
||||
transitions = get_available_FIELD_transitions(
|
||||
application, models.DomainApplication._meta.get_field("status")
|
||||
)
|
||||
|
||||
for transition in transitions:
|
||||
available_transitions.append((transition.target, transition.target))
|
||||
available_transitions.append((transition.target, transition.target.label))
|
||||
|
||||
# only set the available transitions if the user is not restricted
|
||||
# from editing the domain application; otherwise, the form will be
|
||||
|
@ -580,7 +612,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
"No other employees from your organization?",
|
||||
{"fields": ["no_other_contacts_rationale"]},
|
||||
),
|
||||
("Anything else we should know?", {"fields": ["anything_else"]}),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating .gov domains",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
|
@ -600,6 +632,15 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
"is_policy_acknowledged",
|
||||
]
|
||||
|
||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||
|
||||
# lists in filter_horizontal are not sorted properly, sort them
|
||||
# by website
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
if db_field.name in ("current_websites", "alternative_domains"):
|
||||
kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
# Trigger action when a fieldset is changed
|
||||
def save_model(self, request, obj, form, change):
|
||||
if obj and obj.creator.status != models.User.RESTRICTED:
|
||||
|
@ -609,10 +650,10 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
|
||||
if (
|
||||
obj
|
||||
and original_obj.status == models.DomainApplication.APPROVED
|
||||
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
|
||||
and (
|
||||
obj.status == models.DomainApplication.REJECTED
|
||||
or obj.status == models.DomainApplication.INELIGIBLE
|
||||
obj.status == models.DomainApplication.ApplicationStatus.REJECTED
|
||||
or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE
|
||||
)
|
||||
and not obj.domain_is_not_active()
|
||||
):
|
||||
|
@ -634,14 +675,14 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
else:
|
||||
if obj.status != original_obj.status:
|
||||
status_method_mapping = {
|
||||
models.DomainApplication.STARTED: None,
|
||||
models.DomainApplication.SUBMITTED: obj.submit,
|
||||
models.DomainApplication.IN_REVIEW: obj.in_review,
|
||||
models.DomainApplication.ACTION_NEEDED: obj.action_needed,
|
||||
models.DomainApplication.APPROVED: obj.approve,
|
||||
models.DomainApplication.WITHDRAWN: obj.withdraw,
|
||||
models.DomainApplication.REJECTED: obj.reject,
|
||||
models.DomainApplication.INELIGIBLE: (obj.reject_with_prejudice),
|
||||
models.DomainApplication.ApplicationStatus.STARTED: None,
|
||||
models.DomainApplication.ApplicationStatus.SUBMITTED: obj.submit,
|
||||
models.DomainApplication.ApplicationStatus.IN_REVIEW: obj.in_review,
|
||||
models.DomainApplication.ApplicationStatus.ACTION_NEEDED: obj.action_needed,
|
||||
models.DomainApplication.ApplicationStatus.APPROVED: obj.approve,
|
||||
models.DomainApplication.ApplicationStatus.WITHDRAWN: obj.withdraw,
|
||||
models.DomainApplication.ApplicationStatus.REJECTED: obj.reject,
|
||||
models.DomainApplication.ApplicationStatus.INELIGIBLE: (obj.reject_with_prejudice),
|
||||
}
|
||||
selected_method = status_method_mapping.get(obj.status)
|
||||
if selected_method is None:
|
||||
|
@ -711,6 +752,7 @@ class TransitionDomainAdmin(ListHeaderAdmin):
|
|||
"domain_name",
|
||||
"status",
|
||||
"email_sent",
|
||||
"processed",
|
||||
]
|
||||
|
||||
search_fields = ["username", "domain_name"]
|
||||
|
@ -728,6 +770,16 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
fieldsets = DomainInformationAdmin.fieldsets
|
||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
||||
# to activate the edit/delete/view buttons
|
||||
filter_horizontal = ("other_contacts",)
|
||||
|
||||
# lists in filter_horizontal are not sorted properly, sort them
|
||||
# by first_name
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
if db_field.name in ("other_contacts",):
|
||||
kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
||||
|
|
|
@ -47,4 +47,231 @@ function openInNewTab(el, removeAttribute = false){
|
|||
}
|
||||
|
||||
prepareDjangoAdmin();
|
||||
})();
|
||||
})();
|
||||
|
||||
/**
|
||||
* An IIFE to listen to changes on filter_horizontal and enable or disable the change/delete/view buttons as applicable
|
||||
*
|
||||
*/
|
||||
(function extendFilterHorizontalWidgets() {
|
||||
// Initialize custom filter_horizontal widgets; each widget has a "from" select list
|
||||
// and a "to" select list; initialization is based off of the presence of the
|
||||
// "to" select list
|
||||
checkToListThenInitWidget('id_other_contacts_to', 0);
|
||||
checkToListThenInitWidget('id_domain_info-0-other_contacts_to', 0);
|
||||
checkToListThenInitWidget('id_current_websites_to', 0);
|
||||
checkToListThenInitWidget('id_alternative_domains_to', 0);
|
||||
})();
|
||||
|
||||
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,
|
||||
// initialize the associated widget
|
||||
function checkToListThenInitWidget(toListId, attempts) {
|
||||
let toList = document.getElementById(toListId);
|
||||
attempts++;
|
||||
|
||||
if (attempts < 6) {
|
||||
if ((toList !== null)) {
|
||||
// toList found, handle it
|
||||
// Add an event listener on the element
|
||||
// Add disabled buttons on the element's great-grandparent
|
||||
initializeWidgetOnToList(toList, toListId);
|
||||
} else {
|
||||
// Element not found, check again after a delay
|
||||
setTimeout(() => checkToListThenInitWidget(toListId, attempts), 1000); // Check every 1000 milliseconds (1 second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the widget:
|
||||
// add related buttons to the widget for edit, delete and view
|
||||
// add event listeners on the from list, the to list, and selector buttons which either enable or disable the related buttons
|
||||
function initializeWidgetOnToList(toList, toListId) {
|
||||
// create the change button
|
||||
let changeLink = createAndCustomizeLink(
|
||||
toList,
|
||||
toListId,
|
||||
'related-widget-wrapper-link change-related',
|
||||
'Change',
|
||||
'/public/admin/img/icon-changelink.svg',
|
||||
{
|
||||
'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1',
|
||||
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1',
|
||||
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1',
|
||||
},
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
let hasDeletePermission = hasDeletePermissionOnPage();
|
||||
|
||||
let deleteLink = null;
|
||||
if (hasDeletePermission) {
|
||||
// create the delete button if user has permission to delete
|
||||
deleteLink = createAndCustomizeLink(
|
||||
toList,
|
||||
toListId,
|
||||
'related-widget-wrapper-link delete-related',
|
||||
'Delete',
|
||||
'/public/admin/img/icon-deletelink.svg',
|
||||
{
|
||||
'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1',
|
||||
'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
|
||||
'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
|
||||
},
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// create the view button
|
||||
let viewLink = createAndCustomizeLink(
|
||||
toList,
|
||||
toListId,
|
||||
'related-widget-wrapper-link view-related',
|
||||
'View',
|
||||
'/public/admin/img/icon-viewlink.svg',
|
||||
{
|
||||
'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id',
|
||||
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
||||
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
// identify the fromList element in the DOM
|
||||
let fromList = toList.closest('.selector').querySelector(".selector-available select");
|
||||
|
||||
fromList.addEventListener('click', function(event) {
|
||||
handleSelectClick(fromList, changeLink, deleteLink, viewLink);
|
||||
});
|
||||
|
||||
toList.addEventListener('click', function(event) {
|
||||
handleSelectClick(toList, changeLink, deleteLink, viewLink);
|
||||
});
|
||||
|
||||
// Disable buttons when the selectors are interacted with (items are moved from one column to the other)
|
||||
let selectorButtons = [];
|
||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-chooseall"));
|
||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-add"));
|
||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-remove"));
|
||||
|
||||
selectorButtons.forEach((selector) => {
|
||||
selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)});
|
||||
});
|
||||
}
|
||||
|
||||
// create and customize the button, then add to the DOM, relative to the toList
|
||||
// toList - the element in the DOM for the toList
|
||||
// toListId - the ID of the element in the DOM
|
||||
// className - className to add to the created link
|
||||
// action - the action to perform on the item {change, delete, view}
|
||||
// imgSrc - the img.src for the created link
|
||||
// dataMappings - dictionary which relates toListId to href for the created link
|
||||
// dataPopup - boolean for whether the link should produce a popup window
|
||||
// firstPosition - boolean indicating if link should be first position in list of links, otherwise, should be last link
|
||||
function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, firstPosition) {
|
||||
// Create a link element
|
||||
var link = document.createElement('a');
|
||||
|
||||
// Set class attribute for the link
|
||||
link.className = className;
|
||||
|
||||
// Set id
|
||||
// Determine function {change, link, view} from the className
|
||||
// Add {function}_ to the beginning of the string
|
||||
let modifiedLinkString = className.split('-')[0] + '_' + toListId;
|
||||
// Remove '_to' from the end of the string
|
||||
modifiedLinkString = modifiedLinkString.replace('_to', '');
|
||||
link.id = modifiedLinkString;
|
||||
|
||||
// Set data-href-template
|
||||
for (const [idPattern, template] of Object.entries(dataMappings)) {
|
||||
if (toListId.includes(idPattern)) {
|
||||
link.setAttribute('data-href-template', template);
|
||||
break; // Stop checking once a match is found
|
||||
}
|
||||
}
|
||||
|
||||
if (dataPopup)
|
||||
link.setAttribute('data-popup', 'yes');
|
||||
|
||||
link.setAttribute('title-template', action + " selected item")
|
||||
link.title = link.getAttribute('title-template');
|
||||
|
||||
// Create an 'img' element
|
||||
var img = document.createElement('img');
|
||||
|
||||
// Set attributes for the new image
|
||||
img.src = imgSrc;
|
||||
img.alt = action;
|
||||
|
||||
// Append the image to the link
|
||||
link.appendChild(img);
|
||||
|
||||
let relatedWidgetWrapper = toList.closest('.related-widget-wrapper');
|
||||
// If firstPosition is true, insert link as the first child element
|
||||
if (firstPosition) {
|
||||
relatedWidgetWrapper.insertBefore(link, relatedWidgetWrapper.children[0]);
|
||||
} else {
|
||||
// otherwise, insert the link prior to the last child (which is a div)
|
||||
// and also prior to any text elements immediately preceding the last
|
||||
// child node
|
||||
var lastChild = relatedWidgetWrapper.lastChild;
|
||||
|
||||
// Check if lastChild is an element node (not a text node, comment, etc.)
|
||||
if (lastChild.nodeType === 1) {
|
||||
var previousSibling = lastChild.previousSibling;
|
||||
// need to work around some white space which has been inserted into the dom
|
||||
while (previousSibling.nodeType !== 1) {
|
||||
previousSibling = previousSibling.previousSibling;
|
||||
}
|
||||
relatedWidgetWrapper.insertBefore(link, previousSibling.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the link, which we'll use in the disable and enable functions
|
||||
return link;
|
||||
}
|
||||
|
||||
// Either enable or disable widget buttons when select is clicked. Action (enable or disable) taken depends on the count
|
||||
// of selected items in selectElement. If exactly one item is selected, buttons are enabled, and urls for the buttons are
|
||||
// associated with the selected item
|
||||
function handleSelectClick(selectElement, changeLink, deleteLink, viewLink) {
|
||||
|
||||
// If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them
|
||||
if (selectElement.selectedOptions.length === 1) {
|
||||
// enable buttons for selected item in selectElement
|
||||
enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value, selectElement.selectedOptions[0].text);
|
||||
} else {
|
||||
disableRelatedWidgetButtons(changeLink, deleteLink, viewLink);
|
||||
}
|
||||
}
|
||||
|
||||
// return true if there exist elements on the page with classname of delete-related.
|
||||
// presence of one or more of these elements indicates user has permission to delete
|
||||
function hasDeletePermissionOnPage() {
|
||||
return document.querySelector('.delete-related') != null
|
||||
}
|
||||
|
||||
function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) {
|
||||
changeLink.removeAttribute('href');
|
||||
changeLink.setAttribute('title', changeLink.getAttribute('title-template'));
|
||||
if (deleteLink) {
|
||||
deleteLink.removeAttribute('href');
|
||||
deleteLink.setAttribute('title', deleteLink.getAttribute('title-template'));
|
||||
}
|
||||
viewLink.removeAttribute('href');
|
||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template'));
|
||||
}
|
||||
|
||||
function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, elementText) {
|
||||
changeLink.setAttribute('href', changeLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
||||
changeLink.setAttribute('title', changeLink.getAttribute('title-template').replace('selected item', elementText));
|
||||
if (deleteLink) {
|
||||
deleteLink.setAttribute('href', deleteLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
||||
deleteLink.setAttribute('title', deleteLink.getAttribute('title-template').replace('selected item', elementText));
|
||||
}
|
||||
viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText));
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ function _checkDomainAvailability(el) {
|
|||
inlineToast(el.parentElement, el.id, ERROR, response.message);
|
||||
}
|
||||
}
|
||||
fetchJSON(`available/${el.value}`, callback);
|
||||
fetchJSON(`available/?domain=${el.value}`, callback);
|
||||
}
|
||||
|
||||
/** Call the API to see if the domain is good. */
|
||||
|
|
|
@ -37,9 +37,9 @@ body {
|
|||
@include typeset('sans', 'xl', 2);
|
||||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
|
||||
.usa-nav__primary {
|
||||
margin-top: units(1);
|
||||
margin-top:units(1);
|
||||
}
|
||||
|
||||
.section--outlined {
|
||||
|
|
|
@ -33,11 +33,20 @@ env = environs.Env()
|
|||
# Get secrets from Cloud.gov user provided service, if exists
|
||||
# If not, get secrets from environment variables
|
||||
key_service = AppEnv().get_service(name="getgov-credentials")
|
||||
|
||||
|
||||
# Get secrets from Cloud.gov user provided s3 service, if it exists
|
||||
s3_key_service = AppEnv().get_service(name="getgov-s3")
|
||||
|
||||
if key_service and key_service.credentials:
|
||||
if s3_key_service and s3_key_service.credentials:
|
||||
# Concatenate the credentials from our S3 service into our secret service
|
||||
key_service.credentials.update(s3_key_service.credentials)
|
||||
secret = key_service.credentials.get
|
||||
else:
|
||||
secret = env
|
||||
|
||||
|
||||
# # # ###
|
||||
# Values obtained externally #
|
||||
# # # ###
|
||||
|
@ -58,6 +67,12 @@ secret_key = secret("DJANGO_SECRET_KEY")
|
|||
secret_aws_ses_key_id = secret("AWS_ACCESS_KEY_ID", None)
|
||||
secret_aws_ses_key = secret("AWS_SECRET_ACCESS_KEY", None)
|
||||
|
||||
# These keys are present in a getgov-s3 instance, or they can be defined locally
|
||||
aws_s3_region_name = secret("region", None) or secret("AWS_S3_REGION", None)
|
||||
secret_aws_s3_key_id = secret("access_key_id", None) or secret("AWS_S3_ACCESS_KEY_ID", None)
|
||||
secret_aws_s3_key = secret("secret_access_key", None) or secret("AWS_S3_SECRET_ACCESS_KEY", None)
|
||||
secret_aws_s3_bucket_name = secret("bucket", None) or secret("AWS_S3_BUCKET_NAME", None)
|
||||
|
||||
secret_registry_cl_id = secret("REGISTRY_CL_ID")
|
||||
secret_registry_password = secret("REGISTRY_PASSWORD")
|
||||
secret_registry_cert = b64decode(secret("REGISTRY_CERT", ""))
|
||||
|
@ -257,7 +272,14 @@ AUTH_USER_MODEL = "registrar.User"
|
|||
AWS_ACCESS_KEY_ID = secret_aws_ses_key_id
|
||||
AWS_SECRET_ACCESS_KEY = secret_aws_ses_key
|
||||
AWS_REGION = "us-gov-west-1"
|
||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#standard-retry-mode
|
||||
|
||||
# Configuration for accessing AWS S3
|
||||
AWS_S3_ACCESS_KEY_ID = secret_aws_s3_key_id
|
||||
AWS_S3_SECRET_ACCESS_KEY = secret_aws_s3_key
|
||||
AWS_S3_REGION = aws_s3_region_name
|
||||
AWS_S3_BUCKET_NAME = secret_aws_s3_bucket_name
|
||||
|
||||
# https://boto3.amazonaws.com/v1/documentation/latest/guide/retries.html#standard-retry-mode
|
||||
AWS_RETRY_MODE: Final = "standard"
|
||||
# base 2 exponential backoff with max of 20 seconds:
|
||||
AWS_MAX_ATTEMPTS = 3
|
||||
|
@ -518,7 +540,8 @@ OIDC_PROVIDERS = {
|
|||
"response_type": "code",
|
||||
"scope": ["email", "profile:name", "phone"],
|
||||
"user_info_request": ["email", "first_name", "last_name", "phone"],
|
||||
"acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
|
||||
"acr_value": "http://idmanagement.gov/ns/assurance/ial/1",
|
||||
"step_up_acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
|
||||
},
|
||||
"client_registration": {
|
||||
"client_id": "cisa_dotgov_registrar",
|
||||
|
@ -535,7 +558,8 @@ OIDC_PROVIDERS = {
|
|||
"response_type": "code",
|
||||
"scope": ["email", "profile:name", "phone"],
|
||||
"user_info_request": ["email", "first_name", "last_name", "phone"],
|
||||
"acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
|
||||
"acr_value": "http://idmanagement.gov/ns/assurance/ial/1",
|
||||
"step_up_acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
|
||||
},
|
||||
"client_registration": {
|
||||
"client_id": ("urn:gov:cisa:openidconnect.profiles:sp:sso:cisa:dotgov_registrar"),
|
||||
|
|
|
@ -11,7 +11,8 @@ from django.views.generic import RedirectView
|
|||
from registrar import views
|
||||
from registrar.views.application import Step
|
||||
from registrar.views.utility import always_404
|
||||
from api.views import available
|
||||
from api.views import available, get_current_federal, get_current_full
|
||||
|
||||
|
||||
APPLICATION_NAMESPACE = views.ApplicationWizard.URL_NAMESPACE
|
||||
application_urls = [
|
||||
|
@ -72,7 +73,9 @@ urlpatterns = [
|
|||
path("health/", views.health),
|
||||
path("openid/", include("djangooidc.urls")),
|
||||
path("register/", include((application_urls, APPLICATION_NAMESPACE))),
|
||||
path("api/v1/available/<domain>", available, name="available"),
|
||||
path("api/v1/available/", available, name="available"),
|
||||
path("api/v1/get-report/current-federal", get_current_federal, name="get-current-federal"),
|
||||
path("api/v1/get-report/current-full", get_current_full, name="get-current-full"),
|
||||
path(
|
||||
"todo",
|
||||
lambda r: always_404(r, "We forgot to include this link, sorry."),
|
||||
|
|
|
@ -49,28 +49,28 @@ class DomainApplicationFixture:
|
|||
# },
|
||||
DA = [
|
||||
{
|
||||
"status": "started",
|
||||
"organization_name": "Example - Finished but not Submitted",
|
||||
"status": DomainApplication.ApplicationStatus.STARTED,
|
||||
"organization_name": "Example - Finished but not submitted",
|
||||
},
|
||||
{
|
||||
"status": "submitted",
|
||||
"organization_name": "Example - Submitted but pending Investigation",
|
||||
"status": DomainApplication.ApplicationStatus.SUBMITTED,
|
||||
"organization_name": "Example - Submitted but pending investigation",
|
||||
},
|
||||
{
|
||||
"status": "in review",
|
||||
"organization_name": "Example - In Investigation",
|
||||
"status": DomainApplication.ApplicationStatus.IN_REVIEW,
|
||||
"organization_name": "Example - In investigation",
|
||||
},
|
||||
{
|
||||
"status": "in review",
|
||||
"status": DomainApplication.ApplicationStatus.IN_REVIEW,
|
||||
"organization_name": "Example - Approved",
|
||||
},
|
||||
{
|
||||
"status": "withdrawn",
|
||||
"status": DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
"organization_name": "Example - Withdrawn",
|
||||
},
|
||||
{
|
||||
"status": "action needed",
|
||||
"organization_name": "Example - Action Needed",
|
||||
"status": DomainApplication.ApplicationStatus.ACTION_NEEDED,
|
||||
"organization_name": "Example - Action needed",
|
||||
},
|
||||
{
|
||||
"status": "rejected",
|
||||
|
@ -214,7 +214,9 @@ class DomainFixture(DomainApplicationFixture):
|
|||
|
||||
for user in users:
|
||||
# approve one of each users in review status domains
|
||||
application = DomainApplication.objects.filter(creator=user, status=DomainApplication.IN_REVIEW).last()
|
||||
application = DomainApplication.objects.filter(
|
||||
creator=user, status=DomainApplication.ApplicationStatus.IN_REVIEW
|
||||
).last()
|
||||
logger.debug(f"Approving {application} for {user}")
|
||||
application.approve()
|
||||
application.save()
|
||||
|
|
|
@ -244,7 +244,7 @@ class OrganizationContactForm(RegistrarForm):
|
|||
)
|
||||
address_line2 = forms.CharField(
|
||||
required=False,
|
||||
label="Street address line 2",
|
||||
label="Street address line 2 (optional)",
|
||||
)
|
||||
city = forms.CharField(
|
||||
label="City",
|
||||
|
@ -262,13 +262,13 @@ class OrganizationContactForm(RegistrarForm):
|
|||
validators=[
|
||||
RegexValidator(
|
||||
"^[0-9]{5}(?:-[0-9]{4})?$|^$",
|
||||
message="Enter a zip code in the form of 12345 or 12345-6789.",
|
||||
message="Enter a zip code in the required format, like 12345 or 12345-6789.",
|
||||
)
|
||||
],
|
||||
)
|
||||
urbanization = forms.CharField(
|
||||
required=False,
|
||||
label="Urbanization (Puerto Rico only)",
|
||||
label="Urbanization (required for Puerto Rico only)",
|
||||
)
|
||||
|
||||
def clean_federal_agency(self):
|
||||
|
@ -331,7 +331,7 @@ class AuthorizingOfficialForm(RegistrarForm):
|
|||
)
|
||||
middle_name = forms.CharField(
|
||||
required=False,
|
||||
label="Middle name",
|
||||
label="Middle name (optional)",
|
||||
)
|
||||
last_name = forms.CharField(
|
||||
label="Last name / family name",
|
||||
|
@ -407,7 +407,7 @@ class AlternativeDomainForm(RegistrarForm):
|
|||
|
||||
alternative_domain = forms.CharField(
|
||||
required=False,
|
||||
label="Alternative domain",
|
||||
label="",
|
||||
)
|
||||
|
||||
|
||||
|
@ -533,7 +533,7 @@ class YourContactForm(RegistrarForm):
|
|||
)
|
||||
middle_name = forms.CharField(
|
||||
required=False,
|
||||
label="Middle name",
|
||||
label="Middle name (optional)",
|
||||
)
|
||||
last_name = forms.CharField(
|
||||
label="Last name / family name",
|
||||
|
@ -562,7 +562,7 @@ class OtherContactsForm(RegistrarForm):
|
|||
)
|
||||
middle_name = forms.CharField(
|
||||
required=False,
|
||||
label="Middle name",
|
||||
label="Middle name (optional)",
|
||||
)
|
||||
last_name = forms.CharField(
|
||||
label="Last name / family name",
|
||||
|
@ -614,8 +614,8 @@ class NoOtherContactsForm(RegistrarForm):
|
|||
required=True,
|
||||
# label has to end in a space to get the label_suffix to show
|
||||
label=(
|
||||
"Please explain why there are no other employees from your organization"
|
||||
" we can contact to help us assess your eligibility for a .gov domain."
|
||||
"Please explain why there are no other employees from your organization "
|
||||
"we can contact to help us assess your eligibility for a .gov domain."
|
||||
),
|
||||
widget=forms.Textarea(),
|
||||
)
|
||||
|
@ -624,7 +624,7 @@ class NoOtherContactsForm(RegistrarForm):
|
|||
class AnythingElseForm(RegistrarForm):
|
||||
anything_else = forms.CharField(
|
||||
required=False,
|
||||
label="Anything else we should know?",
|
||||
label="Anything else?",
|
||||
widget=forms.Textarea(),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# common.py
|
||||
#
|
||||
# ALGORITHM_CHOICES are options for alg attribute in DS Data
|
||||
# ALGORITHM_CHOICES are options for alg attribute in DS data
|
||||
# reference:
|
||||
# https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
|
||||
ALGORITHM_CHOICES = [
|
||||
|
@ -18,7 +18,7 @@ ALGORITHM_CHOICES = [
|
|||
(15, "(15) Ed25519"),
|
||||
(16, "(16) Ed448"),
|
||||
]
|
||||
# DIGEST_TYPE_CHOICES are options for digestType attribute in DS Data
|
||||
# DIGEST_TYPE_CHOICES are options for digestType attribute in DS data
|
||||
# reference: https://datatracker.ietf.org/doc/html/rfc4034#appendix-A.2
|
||||
DIGEST_TYPE_CHOICES = [
|
||||
(1, "(1) SHA-1"),
|
||||
|
|
|
@ -181,6 +181,9 @@ class ContactForm(forms.ModelForm):
|
|||
for field_name in self.required:
|
||||
self.fields[field_name].required = True
|
||||
|
||||
# Set custom form label
|
||||
self.fields["middle_name"].label = "Middle name (optional)"
|
||||
|
||||
# Set custom error messages
|
||||
self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."}
|
||||
self.fields["last_name"].error_messages = {"required": "Enter your last name / family name."}
|
||||
|
@ -190,7 +193,7 @@ class ContactForm(forms.ModelForm):
|
|||
self.fields["email"].error_messages = {
|
||||
"required": "Enter your email address in the required format, like name@example.com."
|
||||
}
|
||||
self.fields["phone"].error_messages = {"required": "Enter your phone number."}
|
||||
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||
|
||||
|
||||
class AuthorizingOfficialContactForm(ContactForm):
|
||||
|
@ -213,14 +216,14 @@ class AuthorizingOfficialContactForm(ContactForm):
|
|||
self.fields["email"].error_messages = {
|
||||
"required": "Enter an email address in the required format, like name@example.com."
|
||||
}
|
||||
self.fields["phone"].error_messages = {"required": "Enter a phone number for your authorizing official."}
|
||||
self.fields["phone"].error_messages["required"] = "Enter a phone number for your authorizing official."
|
||||
|
||||
|
||||
class DomainSecurityEmailForm(forms.Form):
|
||||
"""Form for adding or editing a security email to a domain."""
|
||||
|
||||
security_email = forms.EmailField(
|
||||
label="Security email",
|
||||
label="Security email (optional)",
|
||||
required=False,
|
||||
error_messages={
|
||||
"invalid": str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)),
|
||||
|
@ -236,7 +239,7 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
|||
validators=[
|
||||
RegexValidator(
|
||||
"^[0-9]{5}(?:-[0-9]{4})?$|^$",
|
||||
message="Enter a zip code in the form of 12345 or 12345-6789.",
|
||||
message="Enter a zip code in the required format, like 12345 or 12345-6789.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -299,7 +302,7 @@ class DomainDnssecForm(forms.Form):
|
|||
|
||||
|
||||
class DomainDsdataForm(forms.Form):
|
||||
"""Form for adding or editing DNSSEC DS Data to a domain."""
|
||||
"""Form for adding or editing DNSSEC DS data to a domain."""
|
||||
|
||||
def validate_hexadecimal(value):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.utility import csv_export
|
||||
from registrar.utility.s3_bucket import S3ClientHelper
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Generates and uploads a current-federal.csv file to our S3 bucket "
|
||||
"which is based off of all existing federal Domains."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add our two filename arguments."""
|
||||
parser.add_argument("--directory", default="migrationdata", help="Desired directory")
|
||||
parser.add_argument(
|
||||
"--checkpath",
|
||||
default=True,
|
||||
help="Flag that determines if we do a check for os.path.exists. Used for test cases",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
"""Grabs the directory then creates current-federal.csv in that directory"""
|
||||
file_name = "current-federal.csv"
|
||||
# Ensures a slash is added
|
||||
directory = os.path.join(options.get("directory"), "")
|
||||
check_path = options.get("checkpath")
|
||||
|
||||
logger.info("Generating report...")
|
||||
try:
|
||||
self.generate_current_federal_report(directory, file_name, check_path)
|
||||
except Exception as err:
|
||||
# TODO - #1317: Notify operations when auto report generation fails
|
||||
raise err
|
||||
else:
|
||||
logger.info(f"Success! Created {file_name}")
|
||||
|
||||
def generate_current_federal_report(self, directory, file_name, check_path):
|
||||
"""Creates a current-full.csv file under the specified directory,
|
||||
then uploads it to a AWS S3 bucket"""
|
||||
s3_client = S3ClientHelper()
|
||||
file_path = os.path.join(directory, file_name)
|
||||
|
||||
# Generate a file locally for upload
|
||||
with open(file_path, "w") as file:
|
||||
csv_export.export_data_federal_to_csv(file)
|
||||
|
||||
if check_path and not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||
|
||||
# Upload this generated file for our S3 instance
|
||||
s3_client.upload_file(file_path, file_name)
|
|
@ -0,0 +1,57 @@
|
|||
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.utility import csv_export
|
||||
from registrar.utility.s3_bucket import S3ClientHelper
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Generates and uploads a current-full.csv file to our S3 bucket " "which is based off of all existing Domains."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add our two filename arguments."""
|
||||
parser.add_argument("--directory", default="migrationdata", help="Desired directory")
|
||||
parser.add_argument(
|
||||
"--checkpath",
|
||||
default=True,
|
||||
help="Flag that determines if we do a check for os.path.exists. Used for test cases",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
"""Grabs the directory then creates current-full.csv in that directory"""
|
||||
file_name = "current-full.csv"
|
||||
# Ensures a slash is added
|
||||
directory = os.path.join(options.get("directory"), "")
|
||||
check_path = options.get("checkpath")
|
||||
|
||||
logger.info("Generating report...")
|
||||
try:
|
||||
self.generate_current_full_report(directory, file_name, check_path)
|
||||
except Exception as err:
|
||||
# TODO - #1317: Notify operations when auto report generation fails
|
||||
raise err
|
||||
else:
|
||||
logger.info(f"Success! Created {file_name}")
|
||||
|
||||
def generate_current_full_report(self, directory, file_name, check_path):
|
||||
"""Creates a current-full.csv file under the specified directory,
|
||||
then uploads it to a AWS S3 bucket"""
|
||||
s3_client = S3ClientHelper()
|
||||
file_path = os.path.join(directory, file_name)
|
||||
|
||||
# Generate a file locally for upload
|
||||
with open(file_path, "w") as file:
|
||||
csv_export.export_data_full_to_csv(file)
|
||||
|
||||
if check_path and not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||
|
||||
# Upload this generated file for our S3 instance
|
||||
s3_client.upload_file(file_path, file_name)
|
|
@ -62,7 +62,7 @@ class Command(BaseCommand):
|
|||
DomainInvitation(
|
||||
email=email_address.lower(),
|
||||
domain=domain,
|
||||
status=DomainInvitation.INVITED,
|
||||
status=DomainInvitation.DomainInvitationStatus.INVITED,
|
||||
)
|
||||
)
|
||||
logger.info("Creating %d invitations", len(to_create))
|
||||
|
|
|
@ -536,19 +536,27 @@ class Command(BaseCommand):
|
|||
domain_name=new_entry_domain_name,
|
||||
)
|
||||
|
||||
if existing_entry.status != new_entry_status:
|
||||
# DEBUG:
|
||||
if not existing_entry.processed:
|
||||
if existing_entry.status != new_entry_status:
|
||||
TerminalHelper.print_conditional(
|
||||
debug_on,
|
||||
f"{TerminalColors.OKCYAN}"
|
||||
f"Updating entry: {existing_entry}"
|
||||
f"Status: {existing_entry.status} > {new_entry_status}" # noqa
|
||||
f"Email Sent: {existing_entry.email_sent} > {new_entry_emailSent}" # noqa
|
||||
f"{TerminalColors.ENDC}",
|
||||
)
|
||||
existing_entry.status = new_entry_status
|
||||
existing_entry.email_sent = new_entry_emailSent
|
||||
existing_entry.save()
|
||||
else:
|
||||
TerminalHelper.print_conditional(
|
||||
debug_on,
|
||||
f"{TerminalColors.OKCYAN}"
|
||||
f"Updating entry: {existing_entry}"
|
||||
f"Status: {existing_entry.status} > {new_entry_status}" # noqa
|
||||
f"Email Sent: {existing_entry.email_sent} > {new_entry_emailSent}" # noqa
|
||||
f"{TerminalColors.YELLOW}"
|
||||
f"Skipping update on processed domain: {existing_entry}"
|
||||
f"{TerminalColors.ENDC}",
|
||||
)
|
||||
existing_entry.status = new_entry_status
|
||||
existing_entry.email_sent = new_entry_emailSent
|
||||
existing_entry.save()
|
||||
|
||||
except TransitionDomain.MultipleObjectsReturned:
|
||||
logger.info(
|
||||
f"{TerminalColors.FAIL}"
|
||||
|
@ -558,6 +566,7 @@ class Command(BaseCommand):
|
|||
f"----------TERMINATING----------"
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
else:
|
||||
# no matching entry, make one
|
||||
new_entry = TransitionDomain(
|
||||
|
@ -565,6 +574,7 @@ class Command(BaseCommand):
|
|||
domain_name=new_entry_domain_name,
|
||||
status=new_entry_status,
|
||||
email_sent=new_entry_emailSent,
|
||||
processed=False,
|
||||
)
|
||||
to_create.append(new_entry)
|
||||
total_new_entries += 1
|
||||
|
|
|
@ -559,7 +559,8 @@ class Command(BaseCommand):
|
|||
debug_max_entries_to_parse,
|
||||
total_rows_parsed,
|
||||
):
|
||||
for transition_domain in TransitionDomain.objects.all():
|
||||
changed_transition_domains = TransitionDomain.objects.filter(processed=False)
|
||||
for transition_domain in changed_transition_domains:
|
||||
(
|
||||
target_domain_information,
|
||||
associated_domain,
|
||||
|
@ -644,7 +645,8 @@ class Command(BaseCommand):
|
|||
debug_max_entries_to_parse,
|
||||
total_rows_parsed,
|
||||
):
|
||||
for transition_domain in TransitionDomain.objects.all():
|
||||
changed_transition_domains = TransitionDomain.objects.filter(processed=False)
|
||||
for transition_domain in changed_transition_domains:
|
||||
# Create some local variables to make data tracing easier
|
||||
transition_domain_name = transition_domain.domain_name
|
||||
transition_domain_status = transition_domain.status
|
||||
|
@ -796,6 +798,7 @@ class Command(BaseCommand):
|
|||
|
||||
# First, save all Domain objects to the database
|
||||
Domain.objects.bulk_create(domains_to_create)
|
||||
|
||||
# DomainInvitation.objects.bulk_create(domain_invitations_to_create)
|
||||
|
||||
# TODO: this is to resolve an error where bulk_create
|
||||
|
@ -847,6 +850,15 @@ class Command(BaseCommand):
|
|||
)
|
||||
DomainInformation.objects.bulk_create(domain_information_to_create)
|
||||
|
||||
# Loop through the list of everything created, and mark it as processed
|
||||
for domain in domains_to_create:
|
||||
name = domain.name
|
||||
TransitionDomain.objects.filter(domain_name=name).update(processed=True)
|
||||
|
||||
# Loop through the list of everything updated, and mark it as processed
|
||||
for name in updated_domain_entries:
|
||||
TransitionDomain.objects.filter(domain_name=name).update(processed=True)
|
||||
|
||||
self.print_summary_of_findings(
|
||||
domains_to_create,
|
||||
updated_domain_entries,
|
||||
|
|
|
@ -155,13 +155,13 @@ class LoadExtraTransitionDomain:
|
|||
def update_transition_domain_models(self):
|
||||
"""Updates TransitionDomain objects based off the file content
|
||||
given in self.parsed_data_container"""
|
||||
all_transition_domains = TransitionDomain.objects.all()
|
||||
if not all_transition_domains.exists():
|
||||
raise ValueError("No TransitionDomain objects exist.")
|
||||
valid_transition_domains = TransitionDomain.objects.filter(processed=False)
|
||||
if not valid_transition_domains.exists():
|
||||
raise ValueError("No updatable TransitionDomain objects exist.")
|
||||
|
||||
updated_transition_domains = []
|
||||
failed_transition_domains = []
|
||||
for transition_domain in all_transition_domains:
|
||||
for transition_domain in valid_transition_domains:
|
||||
domain_name = transition_domain.domain_name
|
||||
updated_transition_domain = transition_domain
|
||||
try:
|
||||
|
@ -228,7 +228,7 @@ class LoadExtraTransitionDomain:
|
|||
# DATA INTEGRITY CHECK
|
||||
# Make sure every Transition Domain got updated
|
||||
total_transition_domains = len(updated_transition_domains)
|
||||
total_updates_made = TransitionDomain.objects.all().count()
|
||||
total_updates_made = TransitionDomain.objects.filter(processed=False).count()
|
||||
if total_transition_domains != total_updates_made:
|
||||
# noqa here for line length
|
||||
logger.error(
|
||||
|
@ -787,7 +787,7 @@ class OrganizationDataLoader:
|
|||
self.tds_to_update: List[TransitionDomain] = []
|
||||
|
||||
def update_organization_data_for_all(self):
|
||||
"""Updates org address data for all TransitionDomains"""
|
||||
"""Updates org address data for valid TransitionDomains"""
|
||||
all_transition_domains = TransitionDomain.objects.all()
|
||||
if len(all_transition_domains) == 0:
|
||||
raise LoadOrganizationError(code=LoadOrganizationErrorCodes.EMPTY_TRANSITION_DOMAIN_TABLE)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.2.7 on 2023-12-05 10:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0048_alter_transitiondomain_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="current_websites",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="current+", to="registrar.website", verbose_name="websites"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="contact_applications", to="registrar.contact", verbose_name="contacts"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_applications_information",
|
||||
to="registrar.contact",
|
||||
verbose_name="contacts",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 4.2.7 on 2023-11-20 20:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0049_alter_domainapplication_current_websites_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="middle_name",
|
||||
field=models.TextField(blank=True, help_text="Middle name (optional)", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="address_line2",
|
||||
field=models.TextField(blank=True, help_text="Street address line 2 (optional)", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="address_line2",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="Street address line 2 (optional)",
|
||||
null=True,
|
||||
verbose_name="Street address line 2 (optional)",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="middle_name",
|
||||
field=models.TextField(blank=True, help_text="Middle name (optional)", null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.2.7 on 2023-11-22 20:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0050_alter_contact_middle_name_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="urbanization",
|
||||
field=models.TextField(blank=True, help_text="Urbanization (required for Puerto Rico only)", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="urbanization",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="Urbanization (required for Puerto Rico only)",
|
||||
null=True,
|
||||
verbose_name="Urbanization (required for Puerto Rico only)",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.2.7 on 2023-11-29 19:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0051_alter_domainapplication_urbanization_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="anything_else",
|
||||
field=models.TextField(blank=True, help_text="Anything else?", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="anything_else",
|
||||
field=models.TextField(blank=True, help_text="Anything else?", null=True),
|
||||
),
|
||||
]
|
37
src/registrar/migrations/0053_create_groups_v05.py
Normal file
37
src/registrar/migrations/0053_create_groups_v05.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0035 (which populates ContentType and Permissions)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0052_alter_domainapplication_anything_else_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,771 @@
|
|||
# Generated by Django 4.2.7 on 2023-11-29 22:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0053_create_groups_v05"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="federal_agency",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(
|
||||
"Administrative Conference of the United States",
|
||||
"Administrative Conference of the United States",
|
||||
),
|
||||
("Advisory Council on Historic Preservation", "Advisory Council on Historic Preservation"),
|
||||
("American Battle Monuments Commission", "American Battle Monuments Commission"),
|
||||
("AMTRAK", "AMTRAK"),
|
||||
("Appalachian Regional Commission", "Appalachian Regional Commission"),
|
||||
(
|
||||
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||
),
|
||||
("Appraisal Subcommittee", "Appraisal Subcommittee"),
|
||||
("Architect of the Capitol", "Architect of the Capitol"),
|
||||
("Armed Forces Retirement Home", "Armed Forces Retirement Home"),
|
||||
(
|
||||
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||
),
|
||||
(
|
||||
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||
),
|
||||
("Central Intelligence Agency", "Central Intelligence Agency"),
|
||||
("Chemical Safety Board", "Chemical Safety Board"),
|
||||
("Christopher Columbus Fellowship Foundation", "Christopher Columbus Fellowship Foundation"),
|
||||
("Civil Rights Cold Case Records Review Board", "Civil Rights Cold Case Records Review Board"),
|
||||
(
|
||||
"Commission for the Preservation of America's Heritage Abroad",
|
||||
"Commission for the Preservation of America's Heritage Abroad",
|
||||
),
|
||||
("Commission of Fine Arts", "Commission of Fine Arts"),
|
||||
(
|
||||
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||
),
|
||||
("Commodity Futures Trading Commission", "Commodity Futures Trading Commission"),
|
||||
("Congressional Budget Office", "Congressional Budget Office"),
|
||||
("Consumer Financial Protection Bureau", "Consumer Financial Protection Bureau"),
|
||||
("Consumer Product Safety Commission", "Consumer Product Safety Commission"),
|
||||
("Corporation for National & Community Service", "Corporation for National & Community Service"),
|
||||
(
|
||||
"Corporation for National and Community Service",
|
||||
"Corporation for National and Community Service",
|
||||
),
|
||||
(
|
||||
"Council of Inspectors General on Integrity and Efficiency",
|
||||
"Council of Inspectors General on Integrity and Efficiency",
|
||||
),
|
||||
("Court Services and Offender Supervision", "Court Services and Offender Supervision"),
|
||||
("Cyberspace Solarium Commission", "Cyberspace Solarium Commission"),
|
||||
(
|
||||
"DC Court Services and Offender Supervision Agency",
|
||||
"DC Court Services and Offender Supervision Agency",
|
||||
),
|
||||
("DC Pre-trial Services", "DC Pre-trial Services"),
|
||||
("Defense Nuclear Facilities Safety Board", "Defense Nuclear Facilities Safety Board"),
|
||||
("Delta Regional Authority", "Delta Regional Authority"),
|
||||
("Denali Commission", "Denali Commission"),
|
||||
("Department of Agriculture", "Department of Agriculture"),
|
||||
("Department of Commerce", "Department of Commerce"),
|
||||
("Department of Defense", "Department of Defense"),
|
||||
("Department of Education", "Department of Education"),
|
||||
("Department of Energy", "Department of Energy"),
|
||||
("Department of Health and Human Services", "Department of Health and Human Services"),
|
||||
("Department of Homeland Security", "Department of Homeland Security"),
|
||||
("Department of Housing and Urban Development", "Department of Housing and Urban Development"),
|
||||
("Department of Justice", "Department of Justice"),
|
||||
("Department of Labor", "Department of Labor"),
|
||||
("Department of State", "Department of State"),
|
||||
("Department of the Interior", "Department of the Interior"),
|
||||
("Department of the Treasury", "Department of the Treasury"),
|
||||
("Department of Transportation", "Department of Transportation"),
|
||||
("Department of Veterans Affairs", "Department of Veterans Affairs"),
|
||||
("Director of National Intelligence", "Director of National Intelligence"),
|
||||
("Dwight D. Eisenhower Memorial Commission", "Dwight D. Eisenhower Memorial Commission"),
|
||||
("Election Assistance Commission", "Election Assistance Commission"),
|
||||
("Environmental Protection Agency", "Environmental Protection Agency"),
|
||||
("Equal Employment Opportunity Commission", "Equal Employment Opportunity Commission"),
|
||||
("Executive Office of the President", "Executive Office of the President"),
|
||||
("Export-Import Bank of the United States", "Export-Import Bank of the United States"),
|
||||
("Export/Import Bank of the U.S.", "Export/Import Bank of the U.S."),
|
||||
("Farm Credit Administration", "Farm Credit Administration"),
|
||||
("Farm Credit System Insurance Corporation", "Farm Credit System Insurance Corporation"),
|
||||
("Federal Communications Commission", "Federal Communications Commission"),
|
||||
("Federal Deposit Insurance Corporation", "Federal Deposit Insurance Corporation"),
|
||||
("Federal Election Commission", "Federal Election Commission"),
|
||||
("Federal Energy Regulatory Commission", "Federal Energy Regulatory Commission"),
|
||||
(
|
||||
"Federal Financial Institutions Examination Council",
|
||||
"Federal Financial Institutions Examination Council",
|
||||
),
|
||||
("Federal Housing Finance Agency", "Federal Housing Finance Agency"),
|
||||
("Federal Judiciary", "Federal Judiciary"),
|
||||
("Federal Labor Relations Authority", "Federal Labor Relations Authority"),
|
||||
("Federal Maritime Commission", "Federal Maritime Commission"),
|
||||
("Federal Mediation and Conciliation Service", "Federal Mediation and Conciliation Service"),
|
||||
(
|
||||
"Federal Mine Safety and Health Review Commission",
|
||||
"Federal Mine Safety and Health Review Commission",
|
||||
),
|
||||
(
|
||||
"Federal Permitting Improvement Steering Council",
|
||||
"Federal Permitting Improvement Steering Council",
|
||||
),
|
||||
("Federal Reserve Board of Governors", "Federal Reserve Board of Governors"),
|
||||
("Federal Reserve System", "Federal Reserve System"),
|
||||
("Federal Trade Commission", "Federal Trade Commission"),
|
||||
("General Services Administration", "General Services Administration"),
|
||||
("gov Administration", "gov Administration"),
|
||||
("Government Accountability Office", "Government Accountability Office"),
|
||||
("Government Publishing Office", "Government Publishing Office"),
|
||||
("Gulf Coast Ecosystem Restoration Council", "Gulf Coast Ecosystem Restoration Council"),
|
||||
("Harry S Truman Scholarship Foundation", "Harry S Truman Scholarship Foundation"),
|
||||
("Harry S. Truman Scholarship Foundation", "Harry S. Truman Scholarship Foundation"),
|
||||
("Institute of Museum and Library Services", "Institute of Museum and Library Services"),
|
||||
("Institute of Peace", "Institute of Peace"),
|
||||
("Inter-American Foundation", "Inter-American Foundation"),
|
||||
(
|
||||
"International Boundary and Water Commission: United States and Mexico",
|
||||
"International Boundary and Water Commission: United States and Mexico",
|
||||
),
|
||||
(
|
||||
"International Boundary Commission: United States and Canada",
|
||||
"International Boundary Commission: United States and Canada",
|
||||
),
|
||||
(
|
||||
"International Joint Commission: United States and Canada",
|
||||
"International Joint Commission: United States and Canada",
|
||||
),
|
||||
("James Madison Memorial Fellowship Foundation", "James Madison Memorial Fellowship Foundation"),
|
||||
("Japan-United States Friendship Commission", "Japan-United States Friendship Commission"),
|
||||
("Japan-US Friendship Commission", "Japan-US Friendship Commission"),
|
||||
("John F. Kennedy Center for Performing Arts", "John F. Kennedy Center for Performing Arts"),
|
||||
(
|
||||
"John F. Kennedy Center for the Performing Arts",
|
||||
"John F. Kennedy Center for the Performing Arts",
|
||||
),
|
||||
("Legal Services Corporation", "Legal Services Corporation"),
|
||||
("Legislative Branch", "Legislative Branch"),
|
||||
("Library of Congress", "Library of Congress"),
|
||||
("Marine Mammal Commission", "Marine Mammal Commission"),
|
||||
(
|
||||
"Medicaid and CHIP Payment and Access Commission",
|
||||
"Medicaid and CHIP Payment and Access Commission",
|
||||
),
|
||||
("Medical Payment Advisory Commission", "Medical Payment Advisory Commission"),
|
||||
("Medicare Payment Advisory Commission", "Medicare Payment Advisory Commission"),
|
||||
("Merit Systems Protection Board", "Merit Systems Protection Board"),
|
||||
("Millennium Challenge Corporation", "Millennium Challenge Corporation"),
|
||||
(
|
||||
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||
),
|
||||
("National Aeronautics and Space Administration", "National Aeronautics and Space Administration"),
|
||||
("National Archives and Records Administration", "National Archives and Records Administration"),
|
||||
("National Capital Planning Commission", "National Capital Planning Commission"),
|
||||
("National Council on Disability", "National Council on Disability"),
|
||||
("National Credit Union Administration", "National Credit Union Administration"),
|
||||
("National Endowment for the Arts", "National Endowment for the Arts"),
|
||||
("National Endowment for the Humanities", "National Endowment for the Humanities"),
|
||||
(
|
||||
"National Foundation on the Arts and the Humanities",
|
||||
"National Foundation on the Arts and the Humanities",
|
||||
),
|
||||
("National Gallery of Art", "National Gallery of Art"),
|
||||
("National Indian Gaming Commission", "National Indian Gaming Commission"),
|
||||
("National Labor Relations Board", "National Labor Relations Board"),
|
||||
("National Mediation Board", "National Mediation Board"),
|
||||
("National Science Foundation", "National Science Foundation"),
|
||||
(
|
||||
"National Security Commission on Artificial Intelligence",
|
||||
"National Security Commission on Artificial Intelligence",
|
||||
),
|
||||
("National Transportation Safety Board", "National Transportation Safety Board"),
|
||||
(
|
||||
"Networking Information Technology Research and Development",
|
||||
"Networking Information Technology Research and Development",
|
||||
),
|
||||
("Non-Federal Agency", "Non-Federal Agency"),
|
||||
("Northern Border Regional Commission", "Northern Border Regional Commission"),
|
||||
("Nuclear Regulatory Commission", "Nuclear Regulatory Commission"),
|
||||
("Nuclear Safety Oversight Committee", "Nuclear Safety Oversight Committee"),
|
||||
("Nuclear Waste Technical Review Board", "Nuclear Waste Technical Review Board"),
|
||||
(
|
||||
"Occupational Safety & Health Review Commission",
|
||||
"Occupational Safety & Health Review Commission",
|
||||
),
|
||||
(
|
||||
"Occupational Safety and Health Review Commission",
|
||||
"Occupational Safety and Health Review Commission",
|
||||
),
|
||||
("Office of Compliance", "Office of Compliance"),
|
||||
("Office of Congressional Workplace Rights", "Office of Congressional Workplace Rights"),
|
||||
("Office of Government Ethics", "Office of Government Ethics"),
|
||||
("Office of Navajo and Hopi Indian Relocation", "Office of Navajo and Hopi Indian Relocation"),
|
||||
("Office of Personnel Management", "Office of Personnel Management"),
|
||||
("Open World Leadership Center", "Open World Leadership Center"),
|
||||
("Overseas Private Investment Corporation", "Overseas Private Investment Corporation"),
|
||||
("Peace Corps", "Peace Corps"),
|
||||
("Pension Benefit Guaranty Corporation", "Pension Benefit Guaranty Corporation"),
|
||||
("Postal Regulatory Commission", "Postal Regulatory Commission"),
|
||||
("Presidio Trust", "Presidio Trust"),
|
||||
("Privacy and Civil Liberties Oversight Board", "Privacy and Civil Liberties Oversight Board"),
|
||||
("Public Buildings Reform Board", "Public Buildings Reform Board"),
|
||||
(
|
||||
"Public Defender Service for the District of Columbia",
|
||||
"Public Defender Service for the District of Columbia",
|
||||
),
|
||||
("Railroad Retirement Board", "Railroad Retirement Board"),
|
||||
("Securities and Exchange Commission", "Securities and Exchange Commission"),
|
||||
("Selective Service System", "Selective Service System"),
|
||||
("Small Business Administration", "Small Business Administration"),
|
||||
("Smithsonian Institution", "Smithsonian Institution"),
|
||||
("Social Security Administration", "Social Security Administration"),
|
||||
("Social Security Advisory Board", "Social Security Advisory Board"),
|
||||
("Southeast Crescent Regional Commission", "Southeast Crescent Regional Commission"),
|
||||
("Southwest Border Regional Commission", "Southwest Border Regional Commission"),
|
||||
("State Justice Institute", "State Justice Institute"),
|
||||
("State, Local, and Tribal Government", "State, Local, and Tribal Government"),
|
||||
("Stennis Center for Public Service", "Stennis Center for Public Service"),
|
||||
("Surface Transportation Board", "Surface Transportation Board"),
|
||||
("Tennessee Valley Authority", "Tennessee Valley Authority"),
|
||||
("The Executive Office of the President", "The Executive Office of the President"),
|
||||
("The Intelligence Community", "The Intelligence Community"),
|
||||
("The Legislative Branch", "The Legislative Branch"),
|
||||
("The Supreme Court", "The Supreme Court"),
|
||||
(
|
||||
"The United States World War One Centennial Commission",
|
||||
"The United States World War One Centennial Commission",
|
||||
),
|
||||
("U.S. Access Board", "U.S. Access Board"),
|
||||
("U.S. Agency for Global Media", "U.S. Agency for Global Media"),
|
||||
("U.S. Agency for International Development", "U.S. Agency for International Development"),
|
||||
("U.S. Capitol Police", "U.S. Capitol Police"),
|
||||
("U.S. Chemical Safety Board", "U.S. Chemical Safety Board"),
|
||||
(
|
||||
"U.S. China Economic and Security Review Commission",
|
||||
"U.S. China Economic and Security Review Commission",
|
||||
),
|
||||
(
|
||||
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||
),
|
||||
("U.S. Commission of Fine Arts", "U.S. Commission of Fine Arts"),
|
||||
("U.S. Commission on Civil Rights", "U.S. Commission on Civil Rights"),
|
||||
(
|
||||
"U.S. Commission on International Religious Freedom",
|
||||
"U.S. Commission on International Religious Freedom",
|
||||
),
|
||||
("U.S. Courts", "U.S. Courts"),
|
||||
("U.S. Department of Agriculture", "U.S. Department of Agriculture"),
|
||||
("U.S. Interagency Council on Homelessness", "U.S. Interagency Council on Homelessness"),
|
||||
("U.S. International Trade Commission", "U.S. International Trade Commission"),
|
||||
("U.S. Nuclear Waste Technical Review Board", "U.S. Nuclear Waste Technical Review Board"),
|
||||
("U.S. Office of Special Counsel", "U.S. Office of Special Counsel"),
|
||||
("U.S. Peace Corps", "U.S. Peace Corps"),
|
||||
("U.S. Postal Service", "U.S. Postal Service"),
|
||||
("U.S. Semiquincentennial Commission", "U.S. Semiquincentennial Commission"),
|
||||
("U.S. Trade and Development Agency", "U.S. Trade and Development Agency"),
|
||||
(
|
||||
"U.S.-China Economic and Security Review Commission",
|
||||
"U.S.-China Economic and Security Review Commission",
|
||||
),
|
||||
("Udall Foundation", "Udall Foundation"),
|
||||
("United States AbilityOne", "United States AbilityOne"),
|
||||
("United States Access Board", "United States Access Board"),
|
||||
("United States African Development Foundation", "United States African Development Foundation"),
|
||||
("United States Agency for Global Media", "United States Agency for Global Media"),
|
||||
("United States Arctic Research Commission", "United States Arctic Research Commission"),
|
||||
("United States Global Change Research Program", "United States Global Change Research Program"),
|
||||
("United States Holocaust Memorial Museum", "United States Holocaust Memorial Museum"),
|
||||
("United States Institute of Peace", "United States Institute of Peace"),
|
||||
(
|
||||
"United States Interagency Council on Homelessness",
|
||||
"United States Interagency Council on Homelessness",
|
||||
),
|
||||
(
|
||||
"United States International Development Finance Corporation",
|
||||
"United States International Development Finance Corporation",
|
||||
),
|
||||
("United States International Trade Commission", "United States International Trade Commission"),
|
||||
("United States Postal Service", "United States Postal Service"),
|
||||
("United States Senate", "United States Senate"),
|
||||
("United States Trade and Development Agency", "United States Trade and Development Agency"),
|
||||
(
|
||||
"Utah Reclamation Mitigation and Conservation Commission",
|
||||
"Utah Reclamation Mitigation and Conservation Commission",
|
||||
),
|
||||
("Vietnam Education Foundation", "Vietnam Education Foundation"),
|
||||
("Western Hemisphere Drug Policy Commission", "Western Hemisphere Drug Policy Commission"),
|
||||
(
|
||||
"Woodrow Wilson International Center for Scholars",
|
||||
"Woodrow Wilson International Center for Scholars",
|
||||
),
|
||||
("World War I Centennial Commission", "World War I Centennial Commission"),
|
||||
],
|
||||
help_text="Federal agency",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
help_text="State, territory, or military post",
|
||||
max_length=2,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="federal_agency",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(
|
||||
"Administrative Conference of the United States",
|
||||
"Administrative Conference of the United States",
|
||||
),
|
||||
("Advisory Council on Historic Preservation", "Advisory Council on Historic Preservation"),
|
||||
("American Battle Monuments Commission", "American Battle Monuments Commission"),
|
||||
("AMTRAK", "AMTRAK"),
|
||||
("Appalachian Regional Commission", "Appalachian Regional Commission"),
|
||||
(
|
||||
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||
),
|
||||
("Appraisal Subcommittee", "Appraisal Subcommittee"),
|
||||
("Architect of the Capitol", "Architect of the Capitol"),
|
||||
("Armed Forces Retirement Home", "Armed Forces Retirement Home"),
|
||||
(
|
||||
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||
),
|
||||
(
|
||||
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||
),
|
||||
("Central Intelligence Agency", "Central Intelligence Agency"),
|
||||
("Chemical Safety Board", "Chemical Safety Board"),
|
||||
("Christopher Columbus Fellowship Foundation", "Christopher Columbus Fellowship Foundation"),
|
||||
("Civil Rights Cold Case Records Review Board", "Civil Rights Cold Case Records Review Board"),
|
||||
(
|
||||
"Commission for the Preservation of America's Heritage Abroad",
|
||||
"Commission for the Preservation of America's Heritage Abroad",
|
||||
),
|
||||
("Commission of Fine Arts", "Commission of Fine Arts"),
|
||||
(
|
||||
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||
),
|
||||
("Commodity Futures Trading Commission", "Commodity Futures Trading Commission"),
|
||||
("Congressional Budget Office", "Congressional Budget Office"),
|
||||
("Consumer Financial Protection Bureau", "Consumer Financial Protection Bureau"),
|
||||
("Consumer Product Safety Commission", "Consumer Product Safety Commission"),
|
||||
("Corporation for National & Community Service", "Corporation for National & Community Service"),
|
||||
(
|
||||
"Corporation for National and Community Service",
|
||||
"Corporation for National and Community Service",
|
||||
),
|
||||
(
|
||||
"Council of Inspectors General on Integrity and Efficiency",
|
||||
"Council of Inspectors General on Integrity and Efficiency",
|
||||
),
|
||||
("Court Services and Offender Supervision", "Court Services and Offender Supervision"),
|
||||
("Cyberspace Solarium Commission", "Cyberspace Solarium Commission"),
|
||||
(
|
||||
"DC Court Services and Offender Supervision Agency",
|
||||
"DC Court Services and Offender Supervision Agency",
|
||||
),
|
||||
("DC Pre-trial Services", "DC Pre-trial Services"),
|
||||
("Defense Nuclear Facilities Safety Board", "Defense Nuclear Facilities Safety Board"),
|
||||
("Delta Regional Authority", "Delta Regional Authority"),
|
||||
("Denali Commission", "Denali Commission"),
|
||||
("Department of Agriculture", "Department of Agriculture"),
|
||||
("Department of Commerce", "Department of Commerce"),
|
||||
("Department of Defense", "Department of Defense"),
|
||||
("Department of Education", "Department of Education"),
|
||||
("Department of Energy", "Department of Energy"),
|
||||
("Department of Health and Human Services", "Department of Health and Human Services"),
|
||||
("Department of Homeland Security", "Department of Homeland Security"),
|
||||
("Department of Housing and Urban Development", "Department of Housing and Urban Development"),
|
||||
("Department of Justice", "Department of Justice"),
|
||||
("Department of Labor", "Department of Labor"),
|
||||
("Department of State", "Department of State"),
|
||||
("Department of the Interior", "Department of the Interior"),
|
||||
("Department of the Treasury", "Department of the Treasury"),
|
||||
("Department of Transportation", "Department of Transportation"),
|
||||
("Department of Veterans Affairs", "Department of Veterans Affairs"),
|
||||
("Director of National Intelligence", "Director of National Intelligence"),
|
||||
("Dwight D. Eisenhower Memorial Commission", "Dwight D. Eisenhower Memorial Commission"),
|
||||
("Election Assistance Commission", "Election Assistance Commission"),
|
||||
("Environmental Protection Agency", "Environmental Protection Agency"),
|
||||
("Equal Employment Opportunity Commission", "Equal Employment Opportunity Commission"),
|
||||
("Executive Office of the President", "Executive Office of the President"),
|
||||
("Export-Import Bank of the United States", "Export-Import Bank of the United States"),
|
||||
("Export/Import Bank of the U.S.", "Export/Import Bank of the U.S."),
|
||||
("Farm Credit Administration", "Farm Credit Administration"),
|
||||
("Farm Credit System Insurance Corporation", "Farm Credit System Insurance Corporation"),
|
||||
("Federal Communications Commission", "Federal Communications Commission"),
|
||||
("Federal Deposit Insurance Corporation", "Federal Deposit Insurance Corporation"),
|
||||
("Federal Election Commission", "Federal Election Commission"),
|
||||
("Federal Energy Regulatory Commission", "Federal Energy Regulatory Commission"),
|
||||
(
|
||||
"Federal Financial Institutions Examination Council",
|
||||
"Federal Financial Institutions Examination Council",
|
||||
),
|
||||
("Federal Housing Finance Agency", "Federal Housing Finance Agency"),
|
||||
("Federal Judiciary", "Federal Judiciary"),
|
||||
("Federal Labor Relations Authority", "Federal Labor Relations Authority"),
|
||||
("Federal Maritime Commission", "Federal Maritime Commission"),
|
||||
("Federal Mediation and Conciliation Service", "Federal Mediation and Conciliation Service"),
|
||||
(
|
||||
"Federal Mine Safety and Health Review Commission",
|
||||
"Federal Mine Safety and Health Review Commission",
|
||||
),
|
||||
(
|
||||
"Federal Permitting Improvement Steering Council",
|
||||
"Federal Permitting Improvement Steering Council",
|
||||
),
|
||||
("Federal Reserve Board of Governors", "Federal Reserve Board of Governors"),
|
||||
("Federal Reserve System", "Federal Reserve System"),
|
||||
("Federal Trade Commission", "Federal Trade Commission"),
|
||||
("General Services Administration", "General Services Administration"),
|
||||
("gov Administration", "gov Administration"),
|
||||
("Government Accountability Office", "Government Accountability Office"),
|
||||
("Government Publishing Office", "Government Publishing Office"),
|
||||
("Gulf Coast Ecosystem Restoration Council", "Gulf Coast Ecosystem Restoration Council"),
|
||||
("Harry S Truman Scholarship Foundation", "Harry S Truman Scholarship Foundation"),
|
||||
("Harry S. Truman Scholarship Foundation", "Harry S. Truman Scholarship Foundation"),
|
||||
("Institute of Museum and Library Services", "Institute of Museum and Library Services"),
|
||||
("Institute of Peace", "Institute of Peace"),
|
||||
("Inter-American Foundation", "Inter-American Foundation"),
|
||||
(
|
||||
"International Boundary and Water Commission: United States and Mexico",
|
||||
"International Boundary and Water Commission: United States and Mexico",
|
||||
),
|
||||
(
|
||||
"International Boundary Commission: United States and Canada",
|
||||
"International Boundary Commission: United States and Canada",
|
||||
),
|
||||
(
|
||||
"International Joint Commission: United States and Canada",
|
||||
"International Joint Commission: United States and Canada",
|
||||
),
|
||||
("James Madison Memorial Fellowship Foundation", "James Madison Memorial Fellowship Foundation"),
|
||||
("Japan-United States Friendship Commission", "Japan-United States Friendship Commission"),
|
||||
("Japan-US Friendship Commission", "Japan-US Friendship Commission"),
|
||||
("John F. Kennedy Center for Performing Arts", "John F. Kennedy Center for Performing Arts"),
|
||||
(
|
||||
"John F. Kennedy Center for the Performing Arts",
|
||||
"John F. Kennedy Center for the Performing Arts",
|
||||
),
|
||||
("Legal Services Corporation", "Legal Services Corporation"),
|
||||
("Legislative Branch", "Legislative Branch"),
|
||||
("Library of Congress", "Library of Congress"),
|
||||
("Marine Mammal Commission", "Marine Mammal Commission"),
|
||||
(
|
||||
"Medicaid and CHIP Payment and Access Commission",
|
||||
"Medicaid and CHIP Payment and Access Commission",
|
||||
),
|
||||
("Medical Payment Advisory Commission", "Medical Payment Advisory Commission"),
|
||||
("Medicare Payment Advisory Commission", "Medicare Payment Advisory Commission"),
|
||||
("Merit Systems Protection Board", "Merit Systems Protection Board"),
|
||||
("Millennium Challenge Corporation", "Millennium Challenge Corporation"),
|
||||
(
|
||||
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||
),
|
||||
("National Aeronautics and Space Administration", "National Aeronautics and Space Administration"),
|
||||
("National Archives and Records Administration", "National Archives and Records Administration"),
|
||||
("National Capital Planning Commission", "National Capital Planning Commission"),
|
||||
("National Council on Disability", "National Council on Disability"),
|
||||
("National Credit Union Administration", "National Credit Union Administration"),
|
||||
("National Endowment for the Arts", "National Endowment for the Arts"),
|
||||
("National Endowment for the Humanities", "National Endowment for the Humanities"),
|
||||
(
|
||||
"National Foundation on the Arts and the Humanities",
|
||||
"National Foundation on the Arts and the Humanities",
|
||||
),
|
||||
("National Gallery of Art", "National Gallery of Art"),
|
||||
("National Indian Gaming Commission", "National Indian Gaming Commission"),
|
||||
("National Labor Relations Board", "National Labor Relations Board"),
|
||||
("National Mediation Board", "National Mediation Board"),
|
||||
("National Science Foundation", "National Science Foundation"),
|
||||
(
|
||||
"National Security Commission on Artificial Intelligence",
|
||||
"National Security Commission on Artificial Intelligence",
|
||||
),
|
||||
("National Transportation Safety Board", "National Transportation Safety Board"),
|
||||
(
|
||||
"Networking Information Technology Research and Development",
|
||||
"Networking Information Technology Research and Development",
|
||||
),
|
||||
("Non-Federal Agency", "Non-Federal Agency"),
|
||||
("Northern Border Regional Commission", "Northern Border Regional Commission"),
|
||||
("Nuclear Regulatory Commission", "Nuclear Regulatory Commission"),
|
||||
("Nuclear Safety Oversight Committee", "Nuclear Safety Oversight Committee"),
|
||||
("Nuclear Waste Technical Review Board", "Nuclear Waste Technical Review Board"),
|
||||
(
|
||||
"Occupational Safety & Health Review Commission",
|
||||
"Occupational Safety & Health Review Commission",
|
||||
),
|
||||
(
|
||||
"Occupational Safety and Health Review Commission",
|
||||
"Occupational Safety and Health Review Commission",
|
||||
),
|
||||
("Office of Compliance", "Office of Compliance"),
|
||||
("Office of Congressional Workplace Rights", "Office of Congressional Workplace Rights"),
|
||||
("Office of Government Ethics", "Office of Government Ethics"),
|
||||
("Office of Navajo and Hopi Indian Relocation", "Office of Navajo and Hopi Indian Relocation"),
|
||||
("Office of Personnel Management", "Office of Personnel Management"),
|
||||
("Open World Leadership Center", "Open World Leadership Center"),
|
||||
("Overseas Private Investment Corporation", "Overseas Private Investment Corporation"),
|
||||
("Peace Corps", "Peace Corps"),
|
||||
("Pension Benefit Guaranty Corporation", "Pension Benefit Guaranty Corporation"),
|
||||
("Postal Regulatory Commission", "Postal Regulatory Commission"),
|
||||
("Presidio Trust", "Presidio Trust"),
|
||||
("Privacy and Civil Liberties Oversight Board", "Privacy and Civil Liberties Oversight Board"),
|
||||
("Public Buildings Reform Board", "Public Buildings Reform Board"),
|
||||
(
|
||||
"Public Defender Service for the District of Columbia",
|
||||
"Public Defender Service for the District of Columbia",
|
||||
),
|
||||
("Railroad Retirement Board", "Railroad Retirement Board"),
|
||||
("Securities and Exchange Commission", "Securities and Exchange Commission"),
|
||||
("Selective Service System", "Selective Service System"),
|
||||
("Small Business Administration", "Small Business Administration"),
|
||||
("Smithsonian Institution", "Smithsonian Institution"),
|
||||
("Social Security Administration", "Social Security Administration"),
|
||||
("Social Security Advisory Board", "Social Security Advisory Board"),
|
||||
("Southeast Crescent Regional Commission", "Southeast Crescent Regional Commission"),
|
||||
("Southwest Border Regional Commission", "Southwest Border Regional Commission"),
|
||||
("State Justice Institute", "State Justice Institute"),
|
||||
("State, Local, and Tribal Government", "State, Local, and Tribal Government"),
|
||||
("Stennis Center for Public Service", "Stennis Center for Public Service"),
|
||||
("Surface Transportation Board", "Surface Transportation Board"),
|
||||
("Tennessee Valley Authority", "Tennessee Valley Authority"),
|
||||
("The Executive Office of the President", "The Executive Office of the President"),
|
||||
("The Intelligence Community", "The Intelligence Community"),
|
||||
("The Legislative Branch", "The Legislative Branch"),
|
||||
("The Supreme Court", "The Supreme Court"),
|
||||
(
|
||||
"The United States World War One Centennial Commission",
|
||||
"The United States World War One Centennial Commission",
|
||||
),
|
||||
("U.S. Access Board", "U.S. Access Board"),
|
||||
("U.S. Agency for Global Media", "U.S. Agency for Global Media"),
|
||||
("U.S. Agency for International Development", "U.S. Agency for International Development"),
|
||||
("U.S. Capitol Police", "U.S. Capitol Police"),
|
||||
("U.S. Chemical Safety Board", "U.S. Chemical Safety Board"),
|
||||
(
|
||||
"U.S. China Economic and Security Review Commission",
|
||||
"U.S. China Economic and Security Review Commission",
|
||||
),
|
||||
(
|
||||
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||
),
|
||||
("U.S. Commission of Fine Arts", "U.S. Commission of Fine Arts"),
|
||||
("U.S. Commission on Civil Rights", "U.S. Commission on Civil Rights"),
|
||||
(
|
||||
"U.S. Commission on International Religious Freedom",
|
||||
"U.S. Commission on International Religious Freedom",
|
||||
),
|
||||
("U.S. Courts", "U.S. Courts"),
|
||||
("U.S. Department of Agriculture", "U.S. Department of Agriculture"),
|
||||
("U.S. Interagency Council on Homelessness", "U.S. Interagency Council on Homelessness"),
|
||||
("U.S. International Trade Commission", "U.S. International Trade Commission"),
|
||||
("U.S. Nuclear Waste Technical Review Board", "U.S. Nuclear Waste Technical Review Board"),
|
||||
("U.S. Office of Special Counsel", "U.S. Office of Special Counsel"),
|
||||
("U.S. Peace Corps", "U.S. Peace Corps"),
|
||||
("U.S. Postal Service", "U.S. Postal Service"),
|
||||
("U.S. Semiquincentennial Commission", "U.S. Semiquincentennial Commission"),
|
||||
("U.S. Trade and Development Agency", "U.S. Trade and Development Agency"),
|
||||
(
|
||||
"U.S.-China Economic and Security Review Commission",
|
||||
"U.S.-China Economic and Security Review Commission",
|
||||
),
|
||||
("Udall Foundation", "Udall Foundation"),
|
||||
("United States AbilityOne", "United States AbilityOne"),
|
||||
("United States Access Board", "United States Access Board"),
|
||||
("United States African Development Foundation", "United States African Development Foundation"),
|
||||
("United States Agency for Global Media", "United States Agency for Global Media"),
|
||||
("United States Arctic Research Commission", "United States Arctic Research Commission"),
|
||||
("United States Global Change Research Program", "United States Global Change Research Program"),
|
||||
("United States Holocaust Memorial Museum", "United States Holocaust Memorial Museum"),
|
||||
("United States Institute of Peace", "United States Institute of Peace"),
|
||||
(
|
||||
"United States Interagency Council on Homelessness",
|
||||
"United States Interagency Council on Homelessness",
|
||||
),
|
||||
(
|
||||
"United States International Development Finance Corporation",
|
||||
"United States International Development Finance Corporation",
|
||||
),
|
||||
("United States International Trade Commission", "United States International Trade Commission"),
|
||||
("United States Postal Service", "United States Postal Service"),
|
||||
("United States Senate", "United States Senate"),
|
||||
("United States Trade and Development Agency", "United States Trade and Development Agency"),
|
||||
(
|
||||
"Utah Reclamation Mitigation and Conservation Commission",
|
||||
"Utah Reclamation Mitigation and Conservation Commission",
|
||||
),
|
||||
("Vietnam Education Foundation", "Vietnam Education Foundation"),
|
||||
("Western Hemisphere Drug Policy Commission", "Western Hemisphere Drug Policy Commission"),
|
||||
(
|
||||
"Woodrow Wilson International Center for Scholars",
|
||||
"Woodrow Wilson International Center for Scholars",
|
||||
),
|
||||
("World War I Centennial Commission", "World War I Centennial Commission"),
|
||||
],
|
||||
help_text="Federal agency",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
help_text="State, territory, or military post",
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="State, territory, or military post",
|
||||
),
|
||||
),
|
||||
]
|
21
src/registrar/migrations/0055_transitiondomain_processed.py
Normal file
21
src/registrar/migrations/0055_transitiondomain_processed.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.2.7 on 2023-12-12 21:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0054_alter_domainapplication_federal_agency_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="transitiondomain",
|
||||
name="processed",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="Indicates whether this TransitionDomain was already processed",
|
||||
verbose_name="Processed",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,70 @@
|
|||
# Generated by Django 4.2.7 on 2023-12-06 16:16
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_fsm
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0055_transitiondomain_processed"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domain",
|
||||
name="state",
|
||||
field=django_fsm.FSMField(
|
||||
choices=[
|
||||
("unknown", "Unknown"),
|
||||
("dns needed", "Dns needed"),
|
||||
("ready", "Ready"),
|
||||
("on hold", "On hold"),
|
||||
("deleted", "Deleted"),
|
||||
],
|
||||
default="unknown",
|
||||
help_text="Very basic info about the lifecycle of this domain object",
|
||||
max_length=21,
|
||||
protected=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="status",
|
||||
field=django_fsm.FSMField(
|
||||
choices=[
|
||||
("started", "Started"),
|
||||
("submitted", "Submitted"),
|
||||
("in review", "In review"),
|
||||
("action needed", "Action needed"),
|
||||
("approved", "Approved"),
|
||||
("withdrawn", "Withdrawn"),
|
||||
("rejected", "Rejected"),
|
||||
("ineligible", "Ineligible"),
|
||||
],
|
||||
default="started",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininvitation",
|
||||
name="status",
|
||||
field=django_fsm.FSMField(
|
||||
choices=[("invited", "Invited"), ("retrieved", "Retrieved")],
|
||||
default="invited",
|
||||
max_length=50,
|
||||
protected=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("ready", "Ready"), ("on hold", "On hold"), ("unknown", "Unknown")],
|
||||
default="ready",
|
||||
help_text="domain status during the transfer",
|
||||
max_length=255,
|
||||
verbose_name="Status",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -38,6 +38,7 @@ auditlog.register(DomainApplication)
|
|||
auditlog.register(Domain)
|
||||
auditlog.register(DraftDomain)
|
||||
auditlog.register(DomainInvitation)
|
||||
auditlog.register(DomainInformation)
|
||||
auditlog.register(HostIP)
|
||||
auditlog.register(Host)
|
||||
auditlog.register(Nameserver)
|
||||
|
|
|
@ -26,7 +26,7 @@ class Contact(TimeStampedModel):
|
|||
middle_name = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Middle name",
|
||||
help_text="Middle name (optional)",
|
||||
)
|
||||
last_name = models.TextField(
|
||||
null=True,
|
||||
|
|
|
@ -122,20 +122,20 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""These capture (some of) the states a domain object can be in."""
|
||||
|
||||
# the state is indeterminate
|
||||
UNKNOWN = "unknown"
|
||||
UNKNOWN = "unknown", "Unknown"
|
||||
|
||||
# The domain object exists in the registry
|
||||
# but nameservers don't exist for it yet
|
||||
DNS_NEEDED = "dns needed"
|
||||
DNS_NEEDED = "dns needed", "Dns needed"
|
||||
|
||||
# Domain has had nameservers set, may or may not be active
|
||||
READY = "ready"
|
||||
READY = "ready", "Ready"
|
||||
|
||||
# Registrar manually changed state to client hold
|
||||
ON_HOLD = "on hold"
|
||||
ON_HOLD = "on hold", "On hold"
|
||||
|
||||
# previously existed but has been deleted from the registry
|
||||
DELETED = "deleted"
|
||||
DELETED = "deleted", "Deleted"
|
||||
|
||||
class Cache(property):
|
||||
"""
|
||||
|
@ -174,7 +174,8 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""Check if a domain is available."""
|
||||
if not cls.string_could_be_domain(domain):
|
||||
raise ValueError("Not a valid domain: %s" % str(domain))
|
||||
req = commands.CheckDomain([domain])
|
||||
domain_name = domain.lower()
|
||||
req = commands.CheckDomain([domain_name])
|
||||
return registry.send(req, cleaned=True).res_data[0].avail
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -19,25 +19,16 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
"""A registrant's application for a new domain."""
|
||||
|
||||
# #### Constants for choice fields ####
|
||||
STARTED = "started"
|
||||
SUBMITTED = "submitted"
|
||||
IN_REVIEW = "in review"
|
||||
ACTION_NEEDED = "action needed"
|
||||
APPROVED = "approved"
|
||||
WITHDRAWN = "withdrawn"
|
||||
REJECTED = "rejected"
|
||||
INELIGIBLE = "ineligible"
|
||||
STATUS_CHOICES = [
|
||||
(STARTED, STARTED),
|
||||
(SUBMITTED, SUBMITTED),
|
||||
(IN_REVIEW, IN_REVIEW),
|
||||
(ACTION_NEEDED, ACTION_NEEDED),
|
||||
(APPROVED, APPROVED),
|
||||
(WITHDRAWN, WITHDRAWN),
|
||||
(REJECTED, REJECTED),
|
||||
(INELIGIBLE, INELIGIBLE),
|
||||
]
|
||||
# Constants for choice fields
|
||||
class ApplicationStatus(models.TextChoices):
|
||||
STARTED = "started", "Started"
|
||||
SUBMITTED = "submitted", "Submitted"
|
||||
IN_REVIEW = "in review", "In review"
|
||||
ACTION_NEEDED = "action needed", "Action needed"
|
||||
APPROVED = "approved", "Approved"
|
||||
WITHDRAWN = "withdrawn", "Withdrawn"
|
||||
REJECTED = "rejected", "Rejected"
|
||||
INELIGIBLE = "ineligible", "Ineligible"
|
||||
|
||||
class StateTerritoryChoices(models.TextChoices):
|
||||
ALABAMA = "AL", "Alabama (AL)"
|
||||
|
@ -363,8 +354,8 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
# #### Internal fields about the application #####
|
||||
status = FSMField(
|
||||
choices=STATUS_CHOICES, # possible states as an array of constants
|
||||
default=STARTED, # sensible default
|
||||
choices=ApplicationStatus.choices, # possible states as an array of constants
|
||||
default=ApplicationStatus.STARTED, # sensible default
|
||||
protected=False, # can change state directly, particularly in Django admin
|
||||
)
|
||||
# This is the application user who created this application. The contact
|
||||
|
@ -409,6 +400,7 @@ class DomainApplication(TimeStampedModel):
|
|||
)
|
||||
|
||||
federal_agency = models.TextField(
|
||||
choices=AGENCY_CHOICES,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Federal agency",
|
||||
|
@ -442,7 +434,7 @@ class DomainApplication(TimeStampedModel):
|
|||
address_line2 = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Street address line 2",
|
||||
help_text="Street address line 2 (optional)",
|
||||
)
|
||||
city = models.TextField(
|
||||
null=True,
|
||||
|
@ -451,6 +443,7 @@ class DomainApplication(TimeStampedModel):
|
|||
)
|
||||
state_territory = models.CharField(
|
||||
max_length=2,
|
||||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="State, territory, or military post",
|
||||
|
@ -465,7 +458,7 @@ class DomainApplication(TimeStampedModel):
|
|||
urbanization = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Urbanization (Puerto Rico only)",
|
||||
help_text="Urbanization (required for Puerto Rico only)",
|
||||
)
|
||||
|
||||
about_your_organization = models.TextField(
|
||||
|
@ -487,6 +480,7 @@ class DomainApplication(TimeStampedModel):
|
|||
"registrar.Website",
|
||||
blank=True,
|
||||
related_name="current+",
|
||||
verbose_name="websites",
|
||||
)
|
||||
|
||||
approved_domain = models.OneToOneField(
|
||||
|
@ -532,6 +526,7 @@ class DomainApplication(TimeStampedModel):
|
|||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_applications",
|
||||
verbose_name="contacts",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
@ -543,7 +538,7 @@ class DomainApplication(TimeStampedModel):
|
|||
anything_else = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Anything else we should know?",
|
||||
help_text="Anything else?",
|
||||
)
|
||||
|
||||
is_policy_acknowledged = models.BooleanField(
|
||||
|
@ -588,7 +583,11 @@ class DomainApplication(TimeStampedModel):
|
|||
except EmailSendingError:
|
||||
logger.warning("Failed to send confirmation email", exc_info=True)
|
||||
|
||||
@transition(field="status", source=[STARTED, ACTION_NEEDED, WITHDRAWN], target=SUBMITTED)
|
||||
@transition(
|
||||
field="status",
|
||||
source=[ApplicationStatus.STARTED, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.WITHDRAWN],
|
||||
target=ApplicationStatus.SUBMITTED,
|
||||
)
|
||||
def submit(self):
|
||||
"""Submit an application that is started.
|
||||
|
||||
|
@ -614,7 +613,7 @@ class DomainApplication(TimeStampedModel):
|
|||
"emails/submission_confirmation_subject.txt",
|
||||
)
|
||||
|
||||
@transition(field="status", source=SUBMITTED, target=IN_REVIEW)
|
||||
@transition(field="status", source=ApplicationStatus.SUBMITTED, target=ApplicationStatus.IN_REVIEW)
|
||||
def in_review(self):
|
||||
"""Investigate an application that has been submitted.
|
||||
|
||||
|
@ -626,7 +625,11 @@ class DomainApplication(TimeStampedModel):
|
|||
"emails/status_change_in_review_subject.txt",
|
||||
)
|
||||
|
||||
@transition(field="status", source=[IN_REVIEW, REJECTED], target=ACTION_NEEDED)
|
||||
@transition(
|
||||
field="status",
|
||||
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED],
|
||||
target=ApplicationStatus.ACTION_NEEDED,
|
||||
)
|
||||
def action_needed(self):
|
||||
"""Send back an application that is under investigation or rejected.
|
||||
|
||||
|
@ -640,8 +643,13 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[SUBMITTED, IN_REVIEW, REJECTED, INELIGIBLE],
|
||||
target=APPROVED,
|
||||
source=[
|
||||
ApplicationStatus.SUBMITTED,
|
||||
ApplicationStatus.IN_REVIEW,
|
||||
ApplicationStatus.REJECTED,
|
||||
ApplicationStatus.INELIGIBLE,
|
||||
],
|
||||
target=ApplicationStatus.APPROVED,
|
||||
)
|
||||
def approve(self):
|
||||
"""Approve an application that has been submitted.
|
||||
|
@ -674,7 +682,11 @@ class DomainApplication(TimeStampedModel):
|
|||
"emails/status_change_approved_subject.txt",
|
||||
)
|
||||
|
||||
@transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN)
|
||||
@transition(
|
||||
field="status",
|
||||
source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW],
|
||||
target=ApplicationStatus.WITHDRAWN,
|
||||
)
|
||||
def withdraw(self):
|
||||
"""Withdraw an application that has been submitted."""
|
||||
self._send_status_update_email(
|
||||
|
@ -685,8 +697,8 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[IN_REVIEW, APPROVED],
|
||||
target=REJECTED,
|
||||
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
|
||||
target=ApplicationStatus.REJECTED,
|
||||
conditions=[domain_is_not_active],
|
||||
)
|
||||
def reject(self):
|
||||
|
@ -694,7 +706,7 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
As side effects this will delete the domain and domain_information
|
||||
(will cascade), and send an email notification."""
|
||||
if self.status == self.APPROVED:
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
domain_state = self.approved_domain.state
|
||||
# Only reject if it exists on EPP
|
||||
if domain_state != Domain.State.UNKNOWN:
|
||||
|
@ -710,8 +722,8 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[IN_REVIEW, APPROVED],
|
||||
target=INELIGIBLE,
|
||||
source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
|
||||
target=ApplicationStatus.INELIGIBLE,
|
||||
conditions=[domain_is_not_active],
|
||||
)
|
||||
def reject_with_prejudice(self):
|
||||
|
@ -723,7 +735,7 @@ class DomainApplication(TimeStampedModel):
|
|||
permissions classes test against. This will also delete the domain
|
||||
and domain_information (will cascade) when they exist."""
|
||||
|
||||
if self.status == self.APPROVED:
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
domain_state = self.approved_domain.state
|
||||
# Only reject if it exists on EPP
|
||||
if domain_state != Domain.State.UNKNOWN:
|
||||
|
|
|
@ -72,6 +72,7 @@ class DomainInformation(TimeStampedModel):
|
|||
)
|
||||
|
||||
federal_agency = models.TextField(
|
||||
choices=AGENCY_CHOICES,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Federal agency",
|
||||
|
@ -106,8 +107,8 @@ class DomainInformation(TimeStampedModel):
|
|||
address_line2 = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Street address line 2",
|
||||
verbose_name="Street address line 2",
|
||||
help_text="Street address line 2 (optional)",
|
||||
verbose_name="Street address line 2 (optional)",
|
||||
)
|
||||
city = models.TextField(
|
||||
null=True,
|
||||
|
@ -116,6 +117,7 @@ class DomainInformation(TimeStampedModel):
|
|||
)
|
||||
state_territory = models.CharField(
|
||||
max_length=2,
|
||||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="State, territory, or military post",
|
||||
|
@ -131,8 +133,8 @@ class DomainInformation(TimeStampedModel):
|
|||
urbanization = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Urbanization (Puerto Rico only)",
|
||||
verbose_name="Urbanization (Puerto Rico only)",
|
||||
help_text="Urbanization (required for Puerto Rico only)",
|
||||
verbose_name="Urbanization (required for Puerto Rico only)",
|
||||
)
|
||||
|
||||
about_your_organization = models.TextField(
|
||||
|
@ -179,6 +181,7 @@ class DomainInformation(TimeStampedModel):
|
|||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_applications_information",
|
||||
verbose_name="contacts",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
@ -190,7 +193,7 @@ class DomainInformation(TimeStampedModel):
|
|||
anything_else = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Anything else we should know?",
|
||||
help_text="Anything else?",
|
||||
)
|
||||
|
||||
is_policy_acknowledged = models.BooleanField(
|
||||
|
|
|
@ -15,8 +15,10 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class DomainInvitation(TimeStampedModel):
|
||||
INVITED = "invited"
|
||||
RETRIEVED = "retrieved"
|
||||
# Constants for status field
|
||||
class DomainInvitationStatus(models.TextChoices):
|
||||
INVITED = "invited", "Invited"
|
||||
RETRIEVED = "retrieved", "Retrieved"
|
||||
|
||||
email = models.EmailField(
|
||||
null=False,
|
||||
|
@ -31,18 +33,15 @@ class DomainInvitation(TimeStampedModel):
|
|||
)
|
||||
|
||||
status = FSMField(
|
||||
choices=[
|
||||
(INVITED, INVITED),
|
||||
(RETRIEVED, RETRIEVED),
|
||||
],
|
||||
default=INVITED,
|
||||
choices=DomainInvitationStatus.choices,
|
||||
default=DomainInvitationStatus.INVITED,
|
||||
protected=True, # can't alter state except through transition methods!
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Invitation for {self.email} on {self.domain} is {self.status}"
|
||||
|
||||
@transition(field="status", source=INVITED, target=RETRIEVED)
|
||||
@transition(field="status", source=DomainInvitationStatus.INVITED, target=DomainInvitationStatus.RETRIEVED)
|
||||
def retrieve(self):
|
||||
"""When an invitation is retrieved, create the corresponding permission.
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from .utility.time_stamped_model import TimeStampedModel
|
|||
|
||||
class StatusChoices(models.TextChoices):
|
||||
READY = "ready", "Ready"
|
||||
ON_HOLD = "on hold", "On Hold"
|
||||
ON_HOLD = "on hold", "On hold"
|
||||
UNKNOWN = "unknown", "Unknown"
|
||||
|
||||
|
||||
|
@ -43,6 +43,12 @@ class TransitionDomain(TimeStampedModel):
|
|||
verbose_name="email sent",
|
||||
help_text="indicates whether email was sent",
|
||||
)
|
||||
processed = models.BooleanField(
|
||||
null=False,
|
||||
default=True,
|
||||
verbose_name="Processed",
|
||||
help_text="Indicates whether this TransitionDomain was already processed",
|
||||
)
|
||||
organization_type = models.TextField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
|
@ -84,7 +90,7 @@ class TransitionDomain(TimeStampedModel):
|
|||
middle_name = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Middle name",
|
||||
help_text="Middle name (optional)",
|
||||
)
|
||||
last_name = models.TextField(
|
||||
null=True,
|
||||
|
|
|
@ -3,6 +3,8 @@ import logging
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
|
||||
from .domain_invitation import DomainInvitation
|
||||
from .transition_domain import TransitionDomain
|
||||
from .domain import Domain
|
||||
|
@ -64,10 +66,43 @@ class User(AbstractUser):
|
|||
def is_restricted(self):
|
||||
return self.status == self.RESTRICTED
|
||||
|
||||
@classmethod
|
||||
def needs_identity_verification(cls, email, uuid):
|
||||
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
||||
or the full identity PII verification"""
|
||||
|
||||
# An existing user who is a domain manager of a domain (that is,
|
||||
# they have an entry in UserDomainRole for their User)
|
||||
try:
|
||||
existing_user = cls.objects.get(username=uuid)
|
||||
if existing_user and UserDomainRole.objects.filter(user=existing_user).exists():
|
||||
return False
|
||||
except cls.DoesNotExist:
|
||||
# Do nothing when the user is not found, as we're checking for existence.
|
||||
pass
|
||||
except Exception as err:
|
||||
raise err
|
||||
|
||||
# A new incoming user who is a domain manager for one of the domains
|
||||
# that we inputted from Verisign (that is, their email address appears
|
||||
# in the username field of a TransitionDomain)
|
||||
if TransitionDomain.objects.filter(username=email).exists():
|
||||
return False
|
||||
|
||||
# A new incoming user who is being invited to be a domain manager (that is,
|
||||
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
|
||||
invited = DomainInvitation.DomainInvitationStatus.INVITED
|
||||
if DomainInvitation.objects.filter(email=email, status=invited).exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def check_domain_invitations_on_login(self):
|
||||
"""When a user first arrives on the site, we need to retrieve any domain
|
||||
invitations that match their email address."""
|
||||
for invitation in DomainInvitation.objects.filter(email=self.email, status=DomainInvitation.INVITED):
|
||||
for invitation in DomainInvitation.objects.filter(
|
||||
email=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||
):
|
||||
try:
|
||||
invitation.retrieve()
|
||||
invitation.save()
|
||||
|
|
|
@ -61,6 +61,11 @@ class UserGroup(Group):
|
|||
"model": "website",
|
||||
"permissions": ["change_website"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "userdomainrole",
|
||||
"permissions": ["view_userdomainrole", "delete_userdomainrole"],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<link rel="apple-touch-icon" size="180x180"
|
||||
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
|
||||
>
|
||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
<p class="text-semibold"><abbr class="usa-hint usa-hint--required" title="required">*</abbr>This question is required.</p>
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>Is there anything else we should know about your domain request?</p>
|
||||
<p>Is there anything else you'd like us to know about your domain request? This question is optional.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% block form_instructions %}
|
||||
<p>Enter your organization’s current public website, if you have one. For example,
|
||||
www.city.com. We can better evaluate your domain request if we know about domains
|
||||
you’re already using. If you already have any .gov domains please include them.</p>
|
||||
you’re already using. If you already have any .gov domains please include them. This question is optional.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Thanks for your domain request!{% endblock %}
|
||||
{% block title %}Thanks for your domain request! | {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container register-form-step">
|
||||
|
|
|
@ -38,8 +38,7 @@
|
|||
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>What .gov domain do you want? <abbr class="usa-hint usa-hint--required
|
||||
text-super" title="required">*</abbr></h2>
|
||||
<h2>What .gov domain do you want?</h2>
|
||||
</legend>
|
||||
|
||||
<p id="domain_instructions" class="margin-top-05">After you enter your domain, we’ll make sure it’s
|
||||
|
@ -47,8 +46,6 @@
|
|||
these initial checks, we’ll verify that it meets all of our requirements once you
|
||||
complete and submit the rest of this form.</p>
|
||||
|
||||
<p class="text-semibold"><abbr class="usa-hint usa-hint--required" title="required">*</abbr> This question is required.</p>
|
||||
|
||||
{% with attr_aria_describedby="domain_instructions domain_instructions2" %}
|
||||
{# attr_validate / validate="domain" invokes code in get-gov.js #}
|
||||
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
||||
|
@ -66,11 +63,11 @@
|
|||
|
||||
<fieldset class="usa-fieldset margin-top-1">
|
||||
<legend>
|
||||
<h2>Alternative domains</h2>
|
||||
<h2>Alternative domains (optional)</h2>
|
||||
</legend>
|
||||
|
||||
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
||||
you your first choice? Entering alternative domains is optional.</p>
|
||||
you your first choice?</p>
|
||||
|
||||
{% with attr_aria_describedby="alt_domain_instructions" %}
|
||||
{# attr_validate / validate="domain" invokes code in get-gov.js #}
|
||||
|
|
|
@ -56,9 +56,12 @@
|
|||
{% block form_instructions %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{% include "includes/required_fields.html" %}
|
||||
{% endblock %}
|
||||
<!-- The "No other employees from your organization?" page is a one-field form and should not have the required fields sentence -->
|
||||
{% if steps.current != "no_other_contacts" %}
|
||||
{% block form_required_fields_help_text %}
|
||||
{% include "includes/required_fields.html" %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<h2 class="margin-bottom-05">
|
||||
Is your organization an election office? <abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
</h2>
|
||||
<h2 class="margin-bottom-05">Is your organization an election office?</h2>
|
||||
|
||||
<p>An election office is a government entity whose <em>primary</em> responsibility is overseeing elections and/or conducting voter registration.</p>
|
||||
|
||||
|
@ -13,7 +11,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
<p class="text-semibold"><abbr class="usa-hint usa-hint--required" title="required">*</abbr> This question is required.</p>
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
|
|
|
@ -3,15 +3,14 @@
|
|||
|
||||
{% block form_instructions %}
|
||||
<h2 class="margin-bottom-05">
|
||||
Which federal branch is your organization in? <abbr class="usa-hint usa-hint--required text-super" title="required">*</abbr>
|
||||
Which federal branch is your organization in?
|
||||
</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
<p class="text-semibold"><abbr class="usa-hint usa-hint--required" title="required">*</abbr> This question is required.</p>
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block form_fields %}
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.federal_type %}
|
||||
|
|
|
@ -3,16 +3,15 @@
|
|||
|
||||
{% block form_instructions %}
|
||||
<h2 class="margin-bottom-05">
|
||||
What kind of U.S.-based government organization do you represent? <abbr class="usa-hint usa-hint--required text-super" title="required">*</abbr>
|
||||
What kind of U.S.-based government organization do you represent?
|
||||
</h2>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
<p class="text-semibold"><abbr class="usa-hint usa-hint--required" title="required">*</abbr> This question is required.</p>
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block form_fields %}
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.organization_type %}
|
||||
|
|
|
@ -13,18 +13,16 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# there are no required fields on this page so don't show this #}
|
||||
{% include "includes/required_fields.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block form_fields %}
|
||||
{{ forms.0.management_form }}
|
||||
{# forms.0 is a formset and this iterates over its forms #}
|
||||
{% for form in forms.0.forms %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend>
|
||||
<h2>Organization contact {{ forloop.counter }}</h2>
|
||||
<h2>Organization contact {{ forloop.counter }} (optional)</h2>
|
||||
</legend>
|
||||
|
||||
{% input_with_errors form.first_name %}
|
||||
|
|
|
@ -13,11 +13,9 @@ Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{
|
|||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
<p class="text-semibold"><abbr class="usa-hint usa-hint--required" title="required">*</abbr> This question is required.</p>
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block form_fields %}
|
||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.purpose %}
|
||||
|
|
|
@ -50,6 +50,10 @@
|
|||
<p>We understand the critical importance of the availability of .gov domains. Suspending or terminating a .gov domain is reserved for prolonged, unresolved, serious violations where the registrant is non-responsive. We'll make extensive efforts to contact registrants and to identify potential solutions. We'll make reasonable accommodations for remediation timelines based on the severity of the issue.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
Status:
|
||||
</span>
|
||||
{% if domainapplication.status == 'approved' %} Approved
|
||||
{% elif domainapplication.status == 'in review' %} In Review
|
||||
{% elif domainapplication.status == 'in review' %} In review
|
||||
{% elif domainapplication.status == 'rejected' %} Rejected
|
||||
{% elif domainapplication.status == 'submitted' %} Submitted
|
||||
{% elif domainapplication.status == 'ineligible' %} Ineligible
|
||||
|
@ -111,7 +111,7 @@
|
|||
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Anything else we should know' value=domainapplication.anything_else|default:"No" heading_level=heading_level %}
|
||||
{% include "includes/summary_item.html" with title='Anything else?' value=domainapplication.anything_else|default:"No" heading_level=heading_level %}
|
||||
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Withdraw request for {{ domainapplication.requested_domain.name }}{% endblock %}
|
||||
{% block title %}Withdraw request for {{ domainapplication.requested_domain.name }} | {% endblock %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<!doctype html>{# keep this on the first line #}
|
||||
{% load i18n static %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
<html class="no-js" lang="{{ LANGUAGE_CODE }}">
|
||||
|
||||
<head>
|
||||
|
@ -144,13 +146,13 @@
|
|||
<header class="usa-header usa-header-basic">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
{% block logo %}
|
||||
<div class="usa-logo" id="extended-logo">
|
||||
<strong class="usa-logo__text" >
|
||||
<a href="{% url 'home' %}"> .gov </a>
|
||||
</strong>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block logo %}
|
||||
<div class="usa-logo display-inline-block" id="extended-logo">
|
||||
<strong class="usa-logo__text" >
|
||||
<a href="{% url 'home' %}">.gov Registrar </a>
|
||||
</strong>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
</div>
|
||||
{% block usa_nav %}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{% extends 'admin/change_form.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block field_sets %}
|
||||
<div class="submit-row">
|
||||
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
{% else %}
|
||||
{{ field.label }}
|
||||
{% endif %}
|
||||
|
||||
{% if widget.attrs.required %}
|
||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
<!--Don't add asterisk to one-field forms -->
|
||||
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating .gov domains." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
||||
{% else %}
|
||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</{{ label_tag }}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers %}
|
||||
|
||||
{% block title %}Add another user{% endblock %}
|
||||
{% block title %}Add another user | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
<h1>Add a domain manager</h1>
|
||||
|
|
|
@ -5,17 +5,15 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-row">
|
||||
{% if not is_analyst_or_superuser or not analyst_action or analyst_action_location != domain.pk %}
|
||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||
text-primary-darker text-semibold"
|
||||
>
|
||||
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-3">
|
||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||
text-primary-darker text-semibold"
|
||||
>
|
||||
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
||||
</p>
|
||||
|
||||
{% if domain.domain_info %}
|
||||
{% include 'domain_sidebar.html' %}
|
||||
{% endif %}
|
||||
|
@ -42,16 +40,6 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{# messages block is under the back breadcrumb link #}
|
||||
{% if messages %}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
Status:
|
||||
</span>
|
||||
{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%}
|
||||
DNS Needed
|
||||
DNS needed
|
||||
{% else %}
|
||||
{{ domain.state|title }}
|
||||
{% endif %}
|
||||
|
|
|
@ -12,9 +12,11 @@
|
|||
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
|
||||
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
<p><a href="{{ url }}">DNS name servers</a></p>
|
||||
<ul>
|
||||
<li><a href="{{ url }}">Name servers</a></li>
|
||||
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
<p><a href="{{ url }}">DNSSEC</a></p>
|
||||
<li><a href="{{ url }}">DNSSEC</a></li>
|
||||
</ul>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block title %}DS Data | {{ domain.name }} | {% endblock %}
|
||||
{% block title %}DS data | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
{% if domain.dnssecdata is None %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
You have no DS Data added. Enable DNSSEC by adding DS Data.
|
||||
You have no DS data added. Enable DNSSEC by adding DS data.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -16,11 +16,11 @@
|
|||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
<h1>DS Data</h1>
|
||||
<h1>DS data</h1>
|
||||
|
||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||
|
||||
<p>Enter the values given by your DNS provider for DS Data.</p>
|
||||
<p>Enter the values given by your DNS provider for DS data.</p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
|
@ -31,9 +31,9 @@
|
|||
{% for form in formset %}
|
||||
<fieldset class="repeatable-form">
|
||||
|
||||
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
|
||||
<legend class="sr-only">DS data record {{forloop.counter}}</legend>
|
||||
|
||||
<h2 class="margin-top-0">DS Data record {{forloop.counter}}</h2>
|
||||
<h2 class="margin-top-0">DS data record {{forloop.counter}}</h2>
|
||||
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-4">
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
DS Data
|
||||
DS data
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -34,6 +34,6 @@ Other employees from your organization:
|
|||
{% for other in application.other_contacts.all %}
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
|
||||
{% endfor %}{% endif %}{% if application.anything_else %}
|
||||
Anything else we should know?
|
||||
Anything else?
|
||||
{{ application.anything_else }}
|
||||
{% endif %}
|
|
@ -14,7 +14,7 @@ Now that your .gov domain has been approved, there are a few more things to do b
|
|||
YOU MUST ADD DOMAIN NAME SERVER INFORMATION
|
||||
|
||||
Before your .gov domain can be used, you have to connect it to your Domain Name System (DNS) hosting service. At this time, we don’t provide DNS hosting services.
|
||||
Go to the domain management page to add your domain name server information <https://registrar.get.gov/domain/{{ application.id }}/nameservers>.
|
||||
Go to the domain management page to add your domain name server information <https://manage.get.gov/domain/{{ application.approved_domain.id }}/nameservers>.
|
||||
|
||||
Get help with adding your domain name server information <https://get.gov/help/domain-management/#manage-dns-information-for-your-domain>.
|
||||
|
||||
|
@ -23,7 +23,7 @@ ADD DOMAIN MANAGERS, SECURITY EMAIL
|
|||
|
||||
We strongly recommend that you add other points of contact who will help manage your domain. We also recommend that you provide a security email. This email will allow the public to report security issues on your domain. Security emails are made public.
|
||||
|
||||
Go to the domain management page to add domain contacts <https://registrar.get.gov/domain/{{ application.id }}/your-contact-information> and a security email <https://registrar.get.gov/domain/{{ application.id }}/security-email>.
|
||||
Go to the domain management page to add domain contacts <https://manage.get.gov/domain/{{ application.approved_domain.id }}/your-contact-information> and a security email <https://manage.get.gov/domain/{{ application.approved_domain.id }}/security-email>.
|
||||
|
||||
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ NEXT STEPS
|
|||
- We’re reviewing your request. This usually takes 20 business days.
|
||||
|
||||
- You can check the status of your request at any time.
|
||||
<https://registrar.get.gov/application/{{ application.id }}>
|
||||
<https://manage.get.gov/application/{{ application.id }}>
|
||||
|
||||
- We’ll email you with questions or when we complete our review.
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ NEXT STEPS
|
|||
- We’ll review your request. This usually takes 20 business days.
|
||||
|
||||
- You can check the status of your request at any time.
|
||||
<https://registrar.get.gov/application/{{ application.id }}>
|
||||
<https://manage.get.gov/application/{{ application.id }}>
|
||||
|
||||
- We’ll email you with questions or when we complete our review.
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Hello {% endblock %}
|
||||
{% block title %} Home | {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
|
@ -53,7 +53,7 @@
|
|||
<td data-sort-value="{{ domain.created_time|date:"U" }}" data-label="Date created">{{ domain.created_time|date }}</td>
|
||||
<td data-label="Status">
|
||||
{% if domain.state == "unknown" or domain.state == "dns needed"%}
|
||||
DNS Needed
|
||||
DNS needed
|
||||
{% else %}
|
||||
{{ domain.state|title }}
|
||||
{% endif %}
|
||||
|
@ -111,7 +111,7 @@
|
|||
{{ application.requested_domain.name|default:"New domain request" }}
|
||||
</th>
|
||||
<td data-sort-value="{{ application.created_at|date:"U" }}" data-label="Date created">{{ application.created_at|date }}</td>
|
||||
<td data-label="Status">{{ application.status|title }}</td>
|
||||
<td data-label="Status">{{ application.get_status_display }}</td>
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
||||
<a href="{% url 'edit-application' application.pk %}">
|
||||
|
|
|
@ -1,50 +1,62 @@
|
|||
{% if is_federal %}
|
||||
{% if federal_type == 'executive' %}
|
||||
<p>Domain requests from executive branch agencies must be authorized by <strong>Chief Information Officers</strong> or <strong>agency heads</strong>.</p>
|
||||
<p>Domain requests from executive branch agencies are subject to guidance issued by the U.S. Office of Management and Budget. </p>
|
||||
<h3>Executive branch federal agencies</h3>
|
||||
<p>Domain requests from executive branch federal agencies must be authorized by the agency's CIO or the head of the agency.</p>
|
||||
<p>See <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.whitehouse.gov/wp-content/uploads/2023/02/M-23-10-DOTGOV-Act-Guidance.pdf">OMB Memorandum M-23-10</a> for more information.</p>
|
||||
|
||||
{% elif federal_type == 'judicial' %}
|
||||
<p>Domain requests from the U.S. Supreme Court must be authorized by the <strong>director of information technology for the U.S. Supreme Court.</strong></p>
|
||||
<p>Domain requests from other judicial branch agencies must be authorized by the <strong>director</strong> or <strong>Chief Information Officer of the Administrative Office (AO)</strong> of the United States Courts.
|
||||
</p>
|
||||
<h3>Judicial branch federal agencies</h3>
|
||||
<p>Domain requests for judicial branch federal agencies, except the U.S. Supreme Court, must be authorized by the director or CIO of the Administrative Office (AO) of the United States Courts.</p>
|
||||
<p>Domain requests from the U.S. Supreme Court must be authorized by the director of information technology for the U.S. Supreme Court.</p>
|
||||
|
||||
{% elif federal_type == 'legislative' %}
|
||||
<h3> U.S. Senate </h3>
|
||||
<p>Domain requests from the U.S. Senate must come from the <strong>Senate Sergeant at Arms.</strong></p>
|
||||
<h3>Legislative branch federal agencies</h3>
|
||||
|
||||
<h3> U.S. House of Representatives </h3>
|
||||
<p class="">Domain requests from the U.S. House of Representatives must come from the <strong>House Chief Administrative Officer.</strong>
|
||||
<h4>U.S. Senate</h4>
|
||||
<p>Domain requests from the U.S. Senate must come from the Senate Sergeant at Arms.</p>
|
||||
|
||||
<h3> Other legislative branch agencies </h3>
|
||||
<p class="margin-top-1">Domain requests from legislative branch agencies must come from the <strong>agency’s head</strong> or <strong>Chief Information Officer.</strong></p>
|
||||
<p class="margin-top-1">Domain requests from legislative commissions must come from the <strong>head of the commission</strong>, or the <strong>head or Chief Information Officer of the parent agency,</strong> if there is one.
|
||||
</p>
|
||||
<h4>U.S. House of Representatives</h4>
|
||||
<p>Domain requests from the U.S. House of Representatives must come from the House Chief Administrative Officer.</p>
|
||||
|
||||
<h4>Other legislative branch agencies</h4>
|
||||
<p>Domain requests from legislative branch agencies must come from the agency’s head or CIO.</p>
|
||||
<p>Domain requests from legislative commissions must come from the head of the commission, or the head or CIO of the parent agency, if there is one.</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% elif organization_type == 'city' %}
|
||||
<p>Domain requests from cities must be authorized by <strong>the mayor</strong> or the equivalent <strong>highest-elected official.</strong></p>
|
||||
<h3>Cities</h3>
|
||||
<p>Domain requests from cities must be authorized by someone in a role of significant, executive responsibility within the city (mayor, council president, city manager, township/village supervisor, select board chairperson, chief, senior technology officer, or equivalent).</p>
|
||||
|
||||
{% elif organization_type == 'county' %}
|
||||
<p>Domain requests from counties must be authorized by the <strong>chair of the county commission </strong>or <strong>the equivalent highest-elected official.</strong></p>
|
||||
<h3>Counties</h3>
|
||||
<p>Domain requests from counties must be authorized by the commission chair or someone in a role of significant, executive responsibility within the county (county judge, county mayor, parish/borough president, senior technology officer, or equivalent). Other county-level offices (county clerk, sheriff, county auditor, comptroller) may qualify, as well, in some instances.</p>
|
||||
|
||||
{% elif organization_type == 'interstate' %}
|
||||
<p>Domain requests from interstate organizations must be authorized by the <strong>highest-ranking executive</strong> (president, director, chair, or equivalent) or <strong>one of the state’s governors or Chief Information Officers.</strong></p>
|
||||
<h3>Interstate organizations</h3>
|
||||
<p>Domain requests from interstate organizations must be authorized by someone in a role of significant, executive responsibility within the organization (president, director, chair, senior technology officer, or equivalent) or one of the state’s governors or CIOs.</p>
|
||||
|
||||
{% elif organization_type == 'school_district' %}
|
||||
<p>Domain requests from school district governments must be authorized by the <strong>highest-ranking executive (the chair of a school district’s board or a superintendent)</strong>.</p>
|
||||
<h3>School districts</h3>
|
||||
<p>Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).</p>
|
||||
|
||||
{% elif organization_type == 'special_district' %}
|
||||
<p>Domain requests from special districts must be authorized by the <strong>highest-ranking executive (president, director, chair, or equivalent)</strong> or <strong>state Chief Information Officers for state-based organizations</strong>.</p>
|
||||
<h3>Special districts</h3>
|
||||
<p>Domain requests from special districts must be authorized by someone in a role of significant, executive responsibility within the district (CEO, chair, executive director, senior technology officer, or equivalent).
|
||||
</p>
|
||||
|
||||
{% elif organization_type == 'state_or_territory' %}
|
||||
<h3>States and territories: executive branch</h3>
|
||||
<p>Domain requests from states and territories must be authorized by the <strong>governor</strong> or the <strong>state Chief Information Officer.</strong></p>
|
||||
<h3>States and territories: judicial and legislative branches</h3>
|
||||
<p>Domain requests from state legislatures and courts must be authorized by an agency’s <strong>Chief Information Officer</strong> or <strong>highest-ranking executive</strong>.</p>
|
||||
<h3>U.S. states and territories</h3>
|
||||
|
||||
<h4>States and territories: executive branch</h4>
|
||||
<p>Domain requests from states and territories must be authorized by the governor or someone in a role of significant, executive responsibility within the agency (department secretary, senior technology officer, or equivalent).</p>
|
||||
|
||||
<h4>States and territories: judicial and legislative branches</h4>
|
||||
<p>Domain requests from state legislatures and courts must be authorized by an agency’s CIO or someone in a role of significant, executive responsibility within the agency.</p>
|
||||
|
||||
{% elif organization_type == 'tribal' %}
|
||||
<p><strong>Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe</strong>, as recognized by the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.bia.gov/service/tribal-leaders-directory">Bureau of Indian Affairs</a>.</p>
|
||||
<p><strong>Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe</strong>, as determined by the state’s tribal recognition initiative.</p>
|
||||
<h3>Tribal governments</h3>
|
||||
<p>Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.bia.gov/service/tribal-leaders-directory">Bureau of Indian Affairs</a> recognizes.</p>
|
||||
<p>Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.</p>
|
||||
|
||||
{% endif %}
|
||||
|
|
|
@ -26,6 +26,10 @@
|
|||
>
|
||||
<address class="usa-footer__address">
|
||||
<div class="usa-footer__contact-info grid-row grid-gap-md">
|
||||
<div class="grid-col-auto">
|
||||
<a class="usa-link" rel="noopener noreferrer" href="{% url 'home' %}">Manage your domains</a>
|
||||
</div>
|
||||
<span class=""> | </span>
|
||||
<div class="grid-col-auto">
|
||||
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'help/' %}">Help </a>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<p class="margin-top-3">
|
||||
Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).
|
||||
<em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
||||
</p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Edit your User Profile
|
||||
Edit your User Profile |
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -294,7 +294,7 @@ class AuditedAdminMockData:
|
|||
self,
|
||||
domain_type,
|
||||
item_name,
|
||||
status=DomainApplication.STARTED,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
org_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
|
@ -311,7 +311,7 @@ class AuditedAdminMockData:
|
|||
title, email, and username.
|
||||
|
||||
status (str - optional): Defines the status for DomainApplication,
|
||||
e.g. DomainApplication.STARTED
|
||||
e.g. DomainApplication.ApplicationStatus.STARTED
|
||||
|
||||
org_type (str - optional): Sets a domains org_type
|
||||
|
||||
|
@ -344,23 +344,23 @@ class AuditedAdminMockData:
|
|||
full_arg_dict = dict(
|
||||
email="test_mail@mail.com",
|
||||
domain=self.dummy_domain(item_name, True),
|
||||
status=DomainInvitation.INVITED,
|
||||
status=DomainInvitation.DomainInvitationStatus.INVITED,
|
||||
)
|
||||
return full_arg_dict
|
||||
|
||||
def create_full_dummy_domain_application(self, item_name, status=DomainApplication.STARTED):
|
||||
def create_full_dummy_domain_application(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
|
||||
"""Creates a dummy domain application object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.APPLICATION, item_name, status)
|
||||
application = DomainApplication.objects.get_or_create(**domain_application_kwargs)[0]
|
||||
return application
|
||||
|
||||
def create_full_dummy_domain_information(self, item_name, status=DomainApplication.STARTED):
|
||||
def create_full_dummy_domain_information(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
|
||||
"""Creates a dummy domain information object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INFORMATION, item_name, status)
|
||||
application = DomainInformation.objects.get_or_create(**domain_application_kwargs)[0]
|
||||
return application
|
||||
|
||||
def create_full_dummy_domain_invitation(self, item_name, status=DomainApplication.STARTED):
|
||||
def create_full_dummy_domain_invitation(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
|
||||
"""Creates a dummy domain invitation object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INVITATION, item_name, status)
|
||||
application = DomainInvitation.objects.get_or_create(**domain_application_kwargs)[0]
|
||||
|
@ -374,7 +374,7 @@ class AuditedAdminMockData:
|
|||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
status=DomainApplication.STARTED,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
):
|
||||
"""A helper to create a dummy domain application object"""
|
||||
application = None
|
||||
|
@ -455,7 +455,7 @@ def completed_application(
|
|||
has_alternative_gov_domain=True,
|
||||
has_about_your_organization=True,
|
||||
has_anything_else=True,
|
||||
status=DomainApplication.STARTED,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
user=False,
|
||||
name="city.gov",
|
||||
):
|
||||
|
@ -829,10 +829,8 @@ class MockEppLib(TestCase):
|
|||
def mockCheckDomainCommand(self, _request, cleaned):
|
||||
if "gsa.gov" in getattr(_request, "names", None):
|
||||
return self._mockDomainName("gsa.gov", False)
|
||||
elif "GSA.gov" in getattr(_request, "names", None):
|
||||
return self._mockDomainName("GSA.gov", False)
|
||||
elif "igorville.gov" in getattr(_request, "names", None):
|
||||
return self._mockDomainName("igorvilleremixed.gov", True)
|
||||
return self._mockDomainName("igorville.gov", True)
|
||||
elif "top-level-agency.gov" in getattr(_request, "names", None):
|
||||
return self._mockDomainName("top-level-agency.gov", True)
|
||||
elif "city.gov" in getattr(_request, "names", None):
|
||||
|
|
3
src/registrar/tests/data/fake_current_federal.csv
Normal file
3
src/registrar/tests/data/fake_current_federal.csv
Normal file
|
@ -0,0 +1,3 @@
|
|||
Domain name,Domain type,Agency,Organization name,City,State,Security Contact Email
|
||||
cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,
|
||||
ddomain3.gov,Federal,Armed Forces Retirement Home,,,,
|
|
4
src/registrar/tests/data/fake_current_full.csv
Normal file
4
src/registrar/tests/data/fake_current_full.csv
Normal file
|
@ -0,0 +1,4 @@
|
|||
Domain name,Domain type,Agency,Organization name,City,State,Security Contact Email
|
||||
cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,
|
||||
ddomain3.gov,Federal,Armed Forces Retirement Home,,,,
|
||||
adomain2.gov,Interstate,,,,,
|
|
|
@ -61,7 +61,7 @@ class TestDomainAdmin(MockEppLib):
|
|||
Make sure the short name is displaying in admin on the list page
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
application.approve()
|
||||
|
||||
response = self.client.get("/admin/registrar/domain/")
|
||||
|
@ -282,7 +282,7 @@ class TestDomainApplicationAdminForm(TestCase):
|
|||
form = DomainApplicationAdminForm(instance=self.application)
|
||||
|
||||
# Verify that the form choices match the available transitions for started
|
||||
expected_choices = [("started", "started"), ("submitted", "submitted")]
|
||||
expected_choices = [("started", "Started"), ("submitted", "Submitted")]
|
||||
self.assertEqual(form.fields["status"].widget.choices, expected_choices)
|
||||
|
||||
def test_form_choices_when_no_instance(self):
|
||||
|
@ -355,7 +355,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.SUBMITTED
|
||||
application.status = DomainApplication.ApplicationStatus.SUBMITTED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -390,13 +390,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.IN_REVIEW
|
||||
application.status = DomainApplication.ApplicationStatus.IN_REVIEW
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -431,13 +431,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.APPROVED
|
||||
application.status = DomainApplication.ApplicationStatus.APPROVED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -467,13 +467,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.APPROVED
|
||||
application.status = DomainApplication.ApplicationStatus.APPROVED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -492,13 +492,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.ACTION_NEEDED
|
||||
application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -533,13 +533,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.REJECTED
|
||||
application.status = DomainApplication.ApplicationStatus.REJECTED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -569,13 +569,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.INELIGIBLE
|
||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -584,7 +584,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
self.assertEqual(application.creator.status, "restricted")
|
||||
|
||||
def test_readonly_when_restricted_creator(self):
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
application.creator.status = User.RESTRICTED
|
||||
application.creator.save()
|
||||
|
||||
|
@ -662,7 +662,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
def test_saving_when_restricted_creator(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
application.creator.status = User.RESTRICTED
|
||||
application.creator.save()
|
||||
|
||||
|
@ -681,11 +681,11 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
)
|
||||
|
||||
# Assert that the status has not changed
|
||||
self.assertEqual(application.status, DomainApplication.IN_REVIEW)
|
||||
self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
def test_change_view_with_restricted_creator(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
application.creator.status = User.RESTRICTED
|
||||
application.creator.save()
|
||||
|
||||
|
@ -704,7 +704,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
application.approved_domain = domain
|
||||
application.save()
|
||||
|
@ -724,7 +724,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.REJECTED
|
||||
application.status = DomainApplication.ApplicationStatus.REJECTED
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
|
@ -735,7 +735,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
def test_side_effects_when_saving_approved_to_rejected(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
|
||||
application.approved_domain = domain
|
||||
|
@ -756,7 +756,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.REJECTED
|
||||
application.status = DomainApplication.ApplicationStatus.REJECTED
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was never called
|
||||
|
@ -774,7 +774,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
application.approved_domain = domain
|
||||
application.save()
|
||||
|
@ -794,7 +794,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.INELIGIBLE
|
||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
|
@ -805,7 +805,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
def test_side_effects_when_saving_approved_to_ineligible(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
|
||||
application.approved_domain = domain
|
||||
|
@ -826,7 +826,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.INELIGIBLE
|
||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was never called
|
||||
|
@ -877,12 +877,14 @@ class DomainInvitationAdminTest(TestCase):
|
|||
)
|
||||
|
||||
# Assert that the filters are added
|
||||
self.assertContains(response, "invited", count=4)
|
||||
self.assertContains(response, "retrieved", count=4)
|
||||
self.assertContains(response, "invited", count=2)
|
||||
self.assertContains(response, "Invited", count=2)
|
||||
self.assertContains(response, "retrieved", count=2)
|
||||
self.assertContains(response, "Retrieved", count=2)
|
||||
|
||||
# Check for the HTML context specificially
|
||||
invited_html = '<a href="?status__exact=invited">invited</a>'
|
||||
retrieved_html = '<a href="?status__exact=retrieved">retrieved</a>'
|
||||
invited_html = '<a href="?status__exact=invited">Invited</a>'
|
||||
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>'
|
||||
|
||||
self.assertContains(response, invited_html, count=1)
|
||||
self.assertContains(response, retrieved_html, count=1)
|
||||
|
|
|
@ -158,7 +158,7 @@ class TestEmails(TestCase):
|
|||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5557\n\nAnything else we should know?")
|
||||
self.assertRegex(body, r"5557\n\nAnything else?")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_anything_else_spacing(self):
|
||||
|
@ -168,6 +168,6 @@ class TestEmails(TestCase):
|
|||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("Anything else we should know", body)
|
||||
self.assertNotIn("Anything else", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5557\n\n----")
|
||||
|
|
|
@ -30,7 +30,7 @@ class TestFormValidation(MockEppLib):
|
|||
form = OrganizationContactForm(data={"zipcode": "nah"})
|
||||
self.assertEqual(
|
||||
form.errors["zipcode"],
|
||||
["Enter a zip code in the form of 12345 or 12345-6789."],
|
||||
["Enter a zip code in the required format, like 12345 or 12345-6789."],
|
||||
)
|
||||
|
||||
def test_org_contact_zip_valid(self):
|
||||
|
@ -74,6 +74,24 @@ class TestFormValidation(MockEppLib):
|
|||
["Enter the .gov domain you want without any periods."],
|
||||
)
|
||||
|
||||
def test_requested_domain_two_dots_invalid(self):
|
||||
"""don't accept domains that are subdomains"""
|
||||
form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"})
|
||||
self.assertEqual(
|
||||
form.errors["requested_domain"],
|
||||
["Enter the .gov domain you want without any periods."],
|
||||
)
|
||||
form = DotGovDomainForm(data={"requested_domain": ".top-level-agency.gov"})
|
||||
self.assertEqual(
|
||||
form.errors["requested_domain"],
|
||||
["Enter the .gov domain you want without any periods."],
|
||||
)
|
||||
form = DotGovDomainForm(data={"requested_domain": "..gov"})
|
||||
self.assertEqual(
|
||||
form.errors["requested_domain"],
|
||||
["Enter the .gov domain you want without any periods."],
|
||||
)
|
||||
|
||||
def test_requested_domain_invalid_characters(self):
|
||||
"""must be a valid .gov domain name."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "underscores_forever"})
|
||||
|
|
|
@ -41,6 +41,8 @@ class TestGroups(TestCase):
|
|||
"change_draftdomain",
|
||||
"analyst_access_permission",
|
||||
"change_user",
|
||||
"delete_userdomainrole",
|
||||
"view_userdomainrole",
|
||||
"change_website",
|
||||
]
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Can create with just a creator."""
|
||||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user)
|
||||
self.assertEqual(application.status, DomainApplication.STARTED)
|
||||
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
def test_full_create(self):
|
||||
"""Can create with all fields."""
|
||||
|
@ -108,7 +108,7 @@ class TestDomainApplication(TestCase):
|
|||
# no submitter email so this emits a log warning
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
self.assertEqual(application.status, application.SUBMITTED)
|
||||
self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
|
||||
|
||||
def test_submit_sends_email(self):
|
||||
"""Create an application and submit it and see if email was sent."""
|
||||
|
@ -139,7 +139,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status submitted and call submit
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
@ -148,7 +148,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status in review and call submit
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
@ -157,7 +157,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status approved and call submit
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
@ -166,7 +166,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status rejected and call submit
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
@ -175,7 +175,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status ineligible and call submit
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
@ -184,7 +184,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status started and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.STARTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
@ -193,7 +193,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status in review and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
@ -202,7 +202,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status approved and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
@ -211,7 +211,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status action needed and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
@ -220,7 +220,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status rejected and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
@ -229,7 +229,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status withdrawn and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
@ -238,7 +238,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status ineligible and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
@ -247,7 +247,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status started and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.STARTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
@ -256,7 +256,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status submitted and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
@ -265,7 +265,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status action needed and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
@ -274,7 +274,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status approved and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
@ -283,7 +283,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status withdrawn and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
@ -292,7 +292,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status ineligible and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
@ -301,7 +301,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status started and call approve
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.STARTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.approve()
|
||||
|
@ -310,7 +310,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status approved and call approve
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.approve()
|
||||
|
@ -319,7 +319,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status action needed and call approve
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.approve()
|
||||
|
@ -328,7 +328,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status withdrawn and call approve
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.approve()
|
||||
|
@ -337,7 +337,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status started and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.STARTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
@ -346,7 +346,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status approved and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
@ -355,7 +355,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status action needed and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
@ -364,7 +364,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status rejected and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
@ -373,7 +373,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status withdrawn and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
@ -382,7 +382,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status ineligible and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
@ -391,7 +391,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status started and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.STARTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject()
|
||||
|
@ -400,7 +400,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status submitted and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject()
|
||||
|
@ -409,7 +409,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status action needed and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject()
|
||||
|
@ -418,7 +418,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status withdrawn and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject()
|
||||
|
@ -427,7 +427,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status rejected and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject()
|
||||
|
@ -436,7 +436,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status ineligible and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject()
|
||||
|
@ -445,7 +445,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status approved, create a matching domain that
|
||||
is active, and call reject against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
application.approved_domain = domain
|
||||
application.save()
|
||||
|
@ -464,7 +464,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status started and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.STARTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject_with_prejudice()
|
||||
|
@ -473,7 +473,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status submitted and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject_with_prejudice()
|
||||
|
@ -482,7 +482,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status action needed and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject_with_prejudice()
|
||||
|
@ -491,7 +491,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status withdrawn and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject_with_prejudice()
|
||||
|
@ -500,7 +500,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status rejected and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject_with_prejudice()
|
||||
|
@ -509,7 +509,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status ineligible and call reject
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.reject_with_prejudice()
|
||||
|
@ -518,7 +518,7 @@ class TestDomainApplication(TestCase):
|
|||
"""Create an application with status approved, create a matching domain that
|
||||
is active, and call reject_with_prejudice against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
application.approved_domain = domain
|
||||
application.save()
|
||||
|
@ -543,7 +543,7 @@ class TestPermissions(TestCase):
|
|||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
||||
# skip using the submit method
|
||||
application.status = DomainApplication.SUBMITTED
|
||||
application.status = DomainApplication.ApplicationStatus.SUBMITTED
|
||||
application.approve()
|
||||
|
||||
# should be a role for this user
|
||||
|
@ -560,7 +560,7 @@ class TestDomainInfo(TestCase):
|
|||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
||||
# skip using the submit method
|
||||
application.status = DomainApplication.SUBMITTED
|
||||
application.status = DomainApplication.ApplicationStatus.SUBMITTED
|
||||
application.approve()
|
||||
|
||||
# should be an information present for this domain
|
||||
|
@ -597,7 +597,7 @@ class TestInvitations(TestCase):
|
|||
# this is not an error but does produce a console warning
|
||||
with less_console_noise():
|
||||
self.invitation.retrieve()
|
||||
self.assertEqual(self.invitation.status, DomainInvitation.RETRIEVED)
|
||||
self.assertEqual(self.invitation.status, DomainInvitation.DomainInvitationStatus.RETRIEVED)
|
||||
|
||||
def test_retrieve_on_each_login(self):
|
||||
"""A user's authenticate on_each_login callback retrieves their invitations."""
|
||||
|
@ -606,19 +606,15 @@ class TestInvitations(TestCase):
|
|||
|
||||
|
||||
class TestUser(TestCase):
|
||||
"""For now, just test actions that
|
||||
occur on user login."""
|
||||
"""Test actions that occur on user login,
|
||||
test class method that controls how users get validated."""
|
||||
|
||||
def setUp(self):
|
||||
self.email = "mayor@igorville.gov"
|
||||
self.domain_name = "igorvilleInTransition.gov"
|
||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
self.user, _ = User.objects.get_or_create(email=self.email)
|
||||
|
||||
# clean out the roles each time
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
TransitionDomain.objects.get_or_create(username="mayor@igorville.gov", domain_name=self.domain_name)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Domain.objects.all().delete()
|
||||
|
@ -626,6 +622,7 @@ class TestUser(TestCase):
|
|||
DomainInformation.objects.all().delete()
|
||||
TransitionDomain.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
def test_check_transition_domains_without_domains_on_login(self):
|
||||
"""A user's on_each_login callback does not check transition domains.
|
||||
|
@ -634,3 +631,26 @@ class TestUser(TestCase):
|
|||
are created."""
|
||||
self.user.on_each_login()
|
||||
self.assertFalse(Domain.objects.filter(name=self.domain_name).exists())
|
||||
|
||||
def test_identity_verification_with_domain_manager(self):
|
||||
"""A domain manager should return False when tested with class
|
||||
method needs_identity_verification"""
|
||||
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
||||
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||
|
||||
def test_identity_verification_with_transition_user(self):
|
||||
"""A user from the Verisign transition should return False
|
||||
when tested with class method needs_identity_verification"""
|
||||
TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
|
||||
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||
|
||||
def test_identity_verification_with_invited_user(self):
|
||||
"""An invited user should return False when tested with class
|
||||
method needs_identity_verification"""
|
||||
DomainInvitation.objects.get_or_create(email=self.user.email, domain=self.domain)
|
||||
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||
|
||||
def test_identity_verification_with_new_user(self):
|
||||
"""A new user who's neither transitioned nor invited should
|
||||
return True when tested with class method needs_identity_verification"""
|
||||
self.assertTrue(User.needs_identity_verification(self.user.email, self.user.username))
|
||||
|
|
|
@ -261,7 +261,7 @@ class TestDomainCreation(MockEppLib):
|
|||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
||||
# skip using the submit method
|
||||
application.status = DomainApplication.SUBMITTED
|
||||
application.status = DomainApplication.ApplicationStatus.SUBMITTED
|
||||
# transition to approve state
|
||||
application.approve()
|
||||
# should have information present for this domain
|
||||
|
@ -1506,7 +1506,7 @@ class TestRegistrantNameservers(MockEppLib):
|
|||
]
|
||||
|
||||
def test_setting_not_allowed(self):
|
||||
"""Scenario: A domain state is not Ready or DNS Needed
|
||||
"""Scenario: A domain state is not Ready or DNS needed
|
||||
then setting nameservers is not allowed"""
|
||||
domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD)
|
||||
with self.assertRaises(ActionNotAllowed):
|
||||
|
|
|
@ -1,11 +1,219 @@
|
|||
from django.test import TestCase
|
||||
from io import StringIO
|
||||
import csv
|
||||
import io
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
from io import StringIO
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.user import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from registrar.utility.csv_export import export_domains_to_writer
|
||||
from django.core.management import call_command
|
||||
from unittest.mock import MagicMock, call, mock_open, patch
|
||||
from api.views import get_current_federal, get_current_full
|
||||
from django.conf import settings
|
||||
from botocore.exceptions import ClientError
|
||||
import boto3_mocking
|
||||
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
|
||||
|
||||
|
||||
class CsvReportsTest(TestCase):
|
||||
"""Tests to determine if we are uploading our reports correctly"""
|
||||
|
||||
def setUp(self):
|
||||
"""Create fake domain data"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.factory = RequestFactory()
|
||||
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
|
||||
)
|
||||
|
||||
self.domain_1, _ = Domain.objects.get_or_create(name="cdomain1.gov", state=Domain.State.READY)
|
||||
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
|
||||
self.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD)
|
||||
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
|
||||
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
|
||||
|
||||
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
domain=self.domain_1,
|
||||
organization_type="federal",
|
||||
federal_agency="World War I Centennial Commission",
|
||||
federal_type="executive",
|
||||
)
|
||||
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
domain=self.domain_2,
|
||||
organization_type="interstate",
|
||||
)
|
||||
self.domain_information_3, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
domain=self.domain_3,
|
||||
organization_type="federal",
|
||||
federal_agency="Armed Forces Retirement Home",
|
||||
)
|
||||
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
domain=self.domain_4,
|
||||
organization_type="federal",
|
||||
federal_agency="Armed Forces Retirement Home",
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Delete all faked data"""
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_generate_federal_report(self):
|
||||
"""Ensures that we correctly generate current-federal.csv"""
|
||||
mock_client = MagicMock()
|
||||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security Contact Email\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("builtins.open", fake_open):
|
||||
call_command("generate_current_federal_report", checkpath=False)
|
||||
content = fake_open()
|
||||
|
||||
content.write.assert_has_calls(expected_file_content)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_generate_full_report(self):
|
||||
"""Ensures that we correctly generate current-full.csv"""
|
||||
mock_client = MagicMock()
|
||||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security Contact Email\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
call("adomain2.gov,Interstate,,,,, \r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("builtins.open", fake_open):
|
||||
call_command("generate_current_full_report", checkpath=False)
|
||||
content = fake_open()
|
||||
|
||||
content.write.assert_has_calls(expected_file_content)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_not_found_full_report(self):
|
||||
"""Ensures that we get a not found when the report doesn't exist"""
|
||||
|
||||
def side_effect(Bucket, Key):
|
||||
raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object")
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_object.side_effect = side_effect
|
||||
|
||||
response = None
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("boto3.client", return_value=mock_client):
|
||||
with self.assertRaises(S3ClientError) as context:
|
||||
response = self.client.get("/api/v1/get-report/current-full")
|
||||
# Check that the response has status code 500
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
# Check that we get the right error back from the page
|
||||
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_not_found_federal_report(self):
|
||||
"""Ensures that we get a not found when the report doesn't exist"""
|
||||
|
||||
def side_effect(Bucket, Key):
|
||||
raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object")
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_object.side_effect = side_effect
|
||||
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("boto3.client", return_value=mock_client):
|
||||
with self.assertRaises(S3ClientError) as context:
|
||||
response = self.client.get("/api/v1/get-report/current-federal")
|
||||
# Check that the response has status code 500
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
# Check that we get the right error back from the page
|
||||
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_load_federal_report(self):
|
||||
"""Tests the get_current_federal api endpoint"""
|
||||
self.maxDiff = None
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with open("registrar/tests/data/fake_current_federal.csv", "r") as file:
|
||||
file_content = file.read()
|
||||
|
||||
# Mock a recieved file
|
||||
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
request = self.factory.get("/fake-path")
|
||||
response = get_current_federal(request)
|
||||
|
||||
# Check that we are sending the correct calls.
|
||||
# Ensures that we are decoding the file content recieved from AWS.
|
||||
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-federal.csv")]
|
||||
mock_client_instance.assert_has_calls(expected_call)
|
||||
|
||||
# Check that the response has status code 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the response contains what we expect
|
||||
expected_file_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security Contact Email\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,"
|
||||
).encode()
|
||||
|
||||
self.assertEqual(expected_file_content, response.content)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_load_full_report(self):
|
||||
"""Tests the current-federal api link"""
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with open("registrar/tests/data/fake_current_full.csv", "r") as file:
|
||||
file_content = file.read()
|
||||
|
||||
# Mock a recieved file
|
||||
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
request = self.factory.get("/fake-path")
|
||||
response = get_current_full(request)
|
||||
|
||||
# Check that we are sending the correct calls.
|
||||
# Ensures that we are decoding the file content recieved from AWS.
|
||||
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-full.csv")]
|
||||
mock_client_instance.assert_has_calls(expected_call)
|
||||
|
||||
# Check that the response has status code 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the response contains what we expect
|
||||
expected_file_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security Contact Email\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\n"
|
||||
"adomain2.gov,Interstate,,,,,"
|
||||
).encode()
|
||||
|
||||
self.assertEqual(expected_file_content, response.content)
|
||||
|
||||
|
||||
class ExportDataTest(TestCase):
|
||||
|
@ -50,7 +258,6 @@ class ExportDataTest(TestCase):
|
|||
)
|
||||
|
||||
def tearDown(self):
|
||||
# Dummy push - will remove
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
|
|
@ -21,6 +21,155 @@ from registrar.models.contact import Contact
|
|||
from .common import less_console_noise
|
||||
|
||||
|
||||
class TestProcessedMigrations(TestCase):
|
||||
"""This test case class is designed to verify the idempotency of migrations
|
||||
related to domain transitions in the application."""
|
||||
|
||||
def setUp(self):
|
||||
"""Defines the file name of migration_json and the folder its contained in"""
|
||||
self.test_data_file_location = "registrar/tests/data"
|
||||
self.migration_json_filename = "test_migrationFilepaths.json"
|
||||
self.user, _ = User.objects.get_or_create(username="igorvillian")
|
||||
|
||||
def tearDown(self):
|
||||
"""Deletes all DB objects related to migrations"""
|
||||
# Delete domain information
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainInvitation.objects.all().delete()
|
||||
TransitionDomain.objects.all().delete()
|
||||
|
||||
# Delete users
|
||||
User.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
def run_load_domains(self):
|
||||
"""
|
||||
This method executes the load_transition_domain command.
|
||||
|
||||
It uses 'unittest.mock.patch' to mock the TerminalHelper.query_yes_no_exit method,
|
||||
which is a user prompt in the terminal. The mock function always returns True,
|
||||
allowing the test to proceed without manual user input.
|
||||
|
||||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the load_transition_domain command with the specified arguments.
|
||||
"""
|
||||
# noqa here because splitting this up makes it confusing.
|
||||
# ES501
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command(
|
||||
"load_transition_domain",
|
||||
self.migration_json_filename,
|
||||
directory=self.test_data_file_location,
|
||||
)
|
||||
|
||||
def run_transfer_domains(self):
|
||||
"""
|
||||
This method executes the transfer_transition_domains_to_domains command.
|
||||
|
||||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the load_transition_domain command with the specified arguments.
|
||||
"""
|
||||
call_command("transfer_transition_domains_to_domains")
|
||||
|
||||
def test_domain_idempotent(self):
|
||||
"""
|
||||
This test ensures that the domain transfer process
|
||||
is idempotent on Domain and DomainInformation.
|
||||
"""
|
||||
unchanged_domain, _ = Domain.objects.get_or_create(
|
||||
name="testdomain.gov",
|
||||
state=Domain.State.READY,
|
||||
expiration_date=datetime.date(2000, 1, 1),
|
||||
)
|
||||
unchanged_domain_information, _ = DomainInformation.objects.get_or_create(
|
||||
domain=unchanged_domain, organization_name="test org name", creator=self.user
|
||||
)
|
||||
self.run_load_domains()
|
||||
|
||||
# Test that a given TransitionDomain isn't set to "processed"
|
||||
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
|
||||
self.assertFalse(transition_domain_object.processed)
|
||||
|
||||
self.run_transfer_domains()
|
||||
|
||||
# Test that old data isn't corrupted
|
||||
actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
|
||||
actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
|
||||
self.assertEqual(unchanged_domain, actual_unchanged)
|
||||
self.assertEqual(unchanged_domain_information, actual_unchanged_information)
|
||||
|
||||
# Test that a given TransitionDomain is set to "processed" after we transfer domains
|
||||
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
|
||||
self.assertTrue(transition_domain_object.processed)
|
||||
|
||||
# Manually change Domain/DomainInformation objects
|
||||
changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get()
|
||||
changed_domain.expiration_date = datetime.date(1999, 1, 1)
|
||||
|
||||
changed_domain.save()
|
||||
|
||||
changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get()
|
||||
changed_domain_information.organization_name = "changed"
|
||||
|
||||
changed_domain_information.save()
|
||||
|
||||
# Rerun transfer domains
|
||||
self.run_transfer_domains()
|
||||
|
||||
# Test that old data isn't corrupted after running this twice
|
||||
actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
|
||||
actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
|
||||
self.assertEqual(unchanged_domain, actual_unchanged)
|
||||
self.assertEqual(unchanged_domain_information, actual_unchanged_information)
|
||||
|
||||
# Ensure that domain hasn't changed
|
||||
actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get()
|
||||
self.assertEqual(changed_domain, actual_domain)
|
||||
|
||||
# Ensure that DomainInformation hasn't changed
|
||||
actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get()
|
||||
self.assertEqual(changed_domain_information, actual_domain_information)
|
||||
|
||||
def test_transition_domain_is_processed(self):
|
||||
"""
|
||||
This test checks if a domain is correctly marked as processed in the transition.
|
||||
"""
|
||||
old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov")
|
||||
# Asser that old records default to 'True'
|
||||
self.assertTrue(old_transition_domain.processed)
|
||||
|
||||
unchanged_domain, _ = Domain.objects.get_or_create(
|
||||
name="testdomain.gov",
|
||||
state=Domain.State.READY,
|
||||
expiration_date=datetime.date(2000, 1, 1),
|
||||
)
|
||||
unchanged_domain_information, _ = DomainInformation.objects.get_or_create(
|
||||
domain=unchanged_domain, organization_name="test org name", creator=self.user
|
||||
)
|
||||
self.run_load_domains()
|
||||
|
||||
# Test that a given TransitionDomain isn't set to "processed"
|
||||
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
|
||||
self.assertFalse(transition_domain_object.processed)
|
||||
|
||||
self.run_transfer_domains()
|
||||
|
||||
# Test that old data isn't corrupted
|
||||
actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
|
||||
actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
|
||||
self.assertEqual(unchanged_domain, actual_unchanged)
|
||||
self.assertTrue(old_transition_domain.processed)
|
||||
self.assertEqual(unchanged_domain_information, actual_unchanged_information)
|
||||
|
||||
# Test that a given TransitionDomain is set to "processed" after we transfer domains
|
||||
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
|
||||
self.assertTrue(transition_domain_object.processed)
|
||||
|
||||
|
||||
class TestOrganizationMigration(TestCase):
|
||||
def setUp(self):
|
||||
"""Defines the file name of migration_json and the folder its contained in"""
|
||||
|
|
|
@ -110,7 +110,9 @@ class TestURLAuth(TestCase):
|
|||
"/openid/callback",
|
||||
"/openid/callback/login/",
|
||||
"/openid/callback/logout/",
|
||||
"/api/v1/available/whitehouse.gov",
|
||||
"/api/v1/available/",
|
||||
"/api/v1/get-report/current-federal",
|
||||
"/api/v1/get-report/current-full",
|
||||
]
|
||||
|
||||
def assertURLIsProtectedByAuth(self, url):
|
||||
|
|
|
@ -100,7 +100,7 @@ class LoggedInTests(TestWithUser):
|
|||
response = self.client.get("/")
|
||||
# count = 2 because it is also in screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=2)
|
||||
self.assertContains(response, "DNS Needed")
|
||||
self.assertContains(response, "DNS needed")
|
||||
# clean up
|
||||
role.delete()
|
||||
|
||||
|
@ -804,7 +804,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# ---- AO CONTACT PAGE ----
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
ao_page = org_contact_result.follow()
|
||||
self.assertContains(ao_page, "Domain requests from executive branch agencies")
|
||||
self.assertContains(ao_page, "Executive branch federal agencies")
|
||||
|
||||
# Go back to organization type page and change type
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1079,11 +1079,11 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
Make sure the long name is displaying in the application summary
|
||||
page (manage your application)
|
||||
"""
|
||||
completed_application(status=DomainApplication.SUBMITTED, user=self.user)
|
||||
completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
# click the "Edit" link
|
||||
detail_page = home_page.click("Manage")
|
||||
detail_page = home_page.click("Manage", index=0)
|
||||
self.assertContains(detail_page, "Federal: an agency of the U.S. government")
|
||||
|
||||
|
||||
|
@ -1941,19 +1941,19 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
self.assertContains(updated_page, "Enable DNSSEC")
|
||||
|
||||
def test_ds_form_loads_with_no_domain_data(self):
|
||||
"""DNSSEC Add DS Data page loads when there is no
|
||||
"""DNSSEC Add DS data page loads when there is no
|
||||
domain DNSSEC data and shows a button to Add new record"""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id}))
|
||||
self.assertContains(page, "You have no DS Data added")
|
||||
self.assertContains(page, "You have no DS data added")
|
||||
self.assertContains(page, "Add new record")
|
||||
|
||||
def test_ds_form_loads_with_ds_data(self):
|
||||
"""DNSSEC Add DS Data page loads when there is
|
||||
"""DNSSEC Add DS data page loads when there is
|
||||
domain DNSSEC DS data and shows the data"""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
|
||||
self.assertContains(page, "DS Data record 1")
|
||||
self.assertContains(page, "DS data record 1")
|
||||
|
||||
def test_ds_data_form_modal(self):
|
||||
"""When user clicks on save, a modal pops up."""
|
||||
|
@ -1974,7 +1974,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
self.assertContains(response, "Trigger Disable DNSSEC Modal")
|
||||
|
||||
def test_ds_data_form_submits(self):
|
||||
"""DS Data form submits successfully
|
||||
"""DS data form submits successfully
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
|
@ -1991,10 +1991,10 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
self.assertContains(page, "The DS Data records for this domain have been updated.")
|
||||
self.assertContains(page, "The DS data records for this domain have been updated.")
|
||||
|
||||
def test_ds_data_form_invalid(self):
|
||||
"""DS Data form errors with invalid data (missing required fields)
|
||||
"""DS data form errors with invalid data (missing required fields)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
|
@ -2017,7 +2017,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
self.assertContains(result, "Digest is required", count=2, status_code=200)
|
||||
|
||||
def test_ds_data_form_invalid_keytag(self):
|
||||
"""DS Data form errors with invalid data (key tag too large)
|
||||
"""DS data form errors with invalid data (key tag too large)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
|
@ -2040,7 +2040,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
)
|
||||
|
||||
def test_ds_data_form_invalid_digest_chars(self):
|
||||
"""DS Data form errors with invalid data (digest contains non hexadecimal chars)
|
||||
"""DS data form errors with invalid data (digest contains non hexadecimal chars)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
|
@ -2063,7 +2063,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
)
|
||||
|
||||
def test_ds_data_form_invalid_digest_sha1(self):
|
||||
"""DS Data form errors with invalid data (digest is invalid sha-1)
|
||||
"""DS data form errors with invalid data (digest is invalid sha-1)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
|
@ -2086,7 +2086,7 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
)
|
||||
|
||||
def test_ds_data_form_invalid_digest_sha256(self):
|
||||
"""DS Data form errors with invalid data (digest is invalid sha-256)
|
||||
"""DS data form errors with invalid data (digest is invalid sha-256)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
|
@ -2117,13 +2117,13 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
|
||||
def test_application_status(self):
|
||||
"""Checking application status page"""
|
||||
application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
||||
application.save()
|
||||
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
# click the "Manage" link
|
||||
detail_page = home_page.click("Manage")
|
||||
detail_page = home_page.click("Manage", index=0)
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
|
@ -2137,13 +2137,13 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
self.user.status = "ineligible"
|
||||
self.user.save()
|
||||
|
||||
application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
||||
application.save()
|
||||
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
# click the "Manage" link
|
||||
detail_page = home_page.click("Manage")
|
||||
detail_page = home_page.click("Manage", index=0)
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
|
@ -2152,13 +2152,13 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
|
||||
def test_application_withdraw(self):
|
||||
"""Checking application status page"""
|
||||
application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
||||
application.save()
|
||||
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
# click the "Manage" link
|
||||
detail_page = home_page.click("Manage")
|
||||
detail_page = home_page.click("Manage", index=0)
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
|
@ -2182,7 +2182,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
|
||||
def test_application_status_no_permissions(self):
|
||||
"""Can't access applications without being the creator."""
|
||||
application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
|
||||
other_user = User()
|
||||
other_user.save()
|
||||
application.creator = other_user
|
||||
|
@ -2202,7 +2202,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
def test_approved_application_not_in_active_requests(self):
|
||||
"""An approved application is not shown in the Active
|
||||
Requests table on home.html."""
|
||||
application = completed_application(status=DomainApplication.APPROVED, user=self.user)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED, user=self.user)
|
||||
application.save()
|
||||
|
||||
home_page = self.app.get("/")
|
||||
|
|
149
src/registrar/utility/s3_bucket.py
Normal file
149
src/registrar/utility/s3_bucket.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
"""Utilities for accessing an AWS S3 bucket"""
|
||||
|
||||
from enum import IntEnum
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class S3ClientErrorCodes(IntEnum):
|
||||
"""Used for S3ClientError
|
||||
Error code overview:
|
||||
- 1 ACCESS_S3_CLIENT_ERROR
|
||||
- 2 UPLOAD_FILE_ERROR
|
||||
- 3 FILE_NOT_FOUND_ERROR
|
||||
- 4 GET_FILE_ERROR
|
||||
"""
|
||||
|
||||
ACCESS_S3_CLIENT_ERROR = 1
|
||||
UPLOAD_FILE_ERROR = 2
|
||||
FILE_NOT_FOUND_ERROR = 3
|
||||
GET_FILE_ERROR = 4
|
||||
|
||||
|
||||
class S3ClientError(RuntimeError):
|
||||
"""
|
||||
Custom exception class for handling errors related to interactions with the S3 storage service via boto3.client.
|
||||
|
||||
This class maps error codes to human-readable error messages. When an instance of S3ClientError is created,
|
||||
an error code can be passed in to set the error message for that instance.
|
||||
|
||||
Attributes:
|
||||
_error_mapping: A dictionary mapping error codes to error messages.
|
||||
code: The error code for a specific instance of S3ClientError.
|
||||
message: The error message for a specific instance of S3ClientError, determined by the error code.
|
||||
"""
|
||||
|
||||
_error_mapping = {
|
||||
S3ClientErrorCodes.ACCESS_S3_CLIENT_ERROR: "Failed to establish a connection with the storage service.",
|
||||
S3ClientErrorCodes.UPLOAD_FILE_ERROR: "File upload to the storage service failed.",
|
||||
S3ClientErrorCodes.FILE_NOT_FOUND_ERROR: "Requested file not found in the storage service.",
|
||||
S3ClientErrorCodes.GET_FILE_ERROR: (
|
||||
"Retrieval of the requested file from " "the storage service failed due to an unspecified error."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code = code
|
||||
if self.code in self._error_mapping:
|
||||
self.message = self._error_mapping.get(self.code)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.message}"
|
||||
|
||||
|
||||
class S3ClientHelper:
|
||||
"""
|
||||
A helper class for interacting with Amazon S3 via the boto3 client.
|
||||
|
||||
This class simplifies the process of initializing the boto3 client,
|
||||
uploading files to S3, and retrieving files from S3.
|
||||
|
||||
Attributes:
|
||||
boto_client: The boto3 client used to interact with S3.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.boto_client = boto3.client(
|
||||
"s3",
|
||||
region_name=settings.AWS_S3_REGION,
|
||||
aws_access_key_id=settings.AWS_S3_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_S3_SECRET_ACCESS_KEY,
|
||||
config=settings.BOTO_CONFIG,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise S3ClientError(code=S3ClientErrorCodes.ACCESS_S3_CLIENT_ERROR) from exc
|
||||
|
||||
def get_bucket_name(self):
|
||||
"""
|
||||
Retrieves the name of the S3 bucket.
|
||||
|
||||
This method returns the name of the S3 bucket as defined in the application's settings.
|
||||
|
||||
Returns:
|
||||
str: The name of the S3 bucket.
|
||||
"""
|
||||
|
||||
return settings.AWS_S3_BUCKET_NAME
|
||||
|
||||
def upload_file(self, file_path, file_name):
|
||||
"""
|
||||
Uploads a file to the S3 bucket.
|
||||
|
||||
This method attempts to upload a file to the S3 bucket using the boto3 client.
|
||||
If an exception occurs during the upload process, it raises an S3ClientError with an UPLOAD_FILE_ERROR code.
|
||||
|
||||
Args:
|
||||
file_path (str): The path of the file to upload.
|
||||
file_name (str): The name to give to the file in the S3 bucket.
|
||||
|
||||
Returns:
|
||||
dict: The response from the boto3 client's upload_file method.
|
||||
|
||||
Raises:
|
||||
S3ClientError: If the file cannot be uploaded to the S3 bucket.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.boto_client.upload_file(file_path, self.get_bucket_name(), file_name)
|
||||
except Exception as exc:
|
||||
raise S3ClientError(code=S3ClientErrorCodes.UPLOAD_FILE_ERROR) from exc
|
||||
return response
|
||||
|
||||
def get_file(self, file_name, decode_to_utf=False):
|
||||
"""
|
||||
Retrieves a file from the S3 bucket and returns its content.
|
||||
|
||||
This method attempts to retrieve a file from the S3 bucket using the boto3 client.
|
||||
If the file is not found, it raises an S3ClientError with a FILE_NOT_FOUND_ERROR code.
|
||||
For any other exceptions during the retrieval process, it raises an S3ClientError with a GET_FILE_ERROR code.
|
||||
|
||||
Args:
|
||||
file_name (str): The name of the file to retrieve from the S3 bucket.
|
||||
decode_to_utf (bool, optional): If True, the file content is decoded from bytes to a UTF-8 string.
|
||||
Defaults to False.
|
||||
|
||||
Returns:
|
||||
bytes or str: The content of the file. If decode_to_utf is True, this is a string. Otherwise, its bytes.
|
||||
|
||||
Raises:
|
||||
S3ClientError: If the file cannot be retrieved from the S3 bucket.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.boto_client.get_object(Bucket=self.get_bucket_name(), Key=file_name)
|
||||
except ClientError as exc:
|
||||
if exc.response["Error"]["Code"] == "NoSuchKey":
|
||||
raise S3ClientError(code=S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) from exc
|
||||
else:
|
||||
raise S3ClientError(code=S3ClientErrorCodes.GET_FILE_ERROR) from exc
|
||||
except Exception as exc:
|
||||
raise S3ClientError(code=S3ClientErrorCodes.GET_FILE_ERROR) from exc
|
||||
|
||||
file_content = response["Body"].read()
|
||||
if decode_to_utf:
|
||||
return file_content.decode("utf-8")
|
||||
else:
|
||||
return file_content
|
|
@ -86,7 +86,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
Step.YOUR_CONTACT: _("Your contact information"),
|
||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||
Step.NO_OTHER_CONTACTS: _("No other employees from your organization?"),
|
||||
Step.ANYTHING_ELSE: _("Anything else we should know?"),
|
||||
Step.ANYTHING_ELSE: _("Anything else?"),
|
||||
Step.REQUIREMENTS: _("Requirements for operating .gov domains"),
|
||||
Step.REVIEW: _("Review and submit your domain request"),
|
||||
}
|
||||
|
@ -293,9 +293,9 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
return self.pending_applications()
|
||||
|
||||
def approved_applications_exist(self):
|
||||
"""Checks if user is creator of applications with APPROVED status"""
|
||||
"""Checks if user is creator of applications with ApplicationStatus.APPROVED status"""
|
||||
approved_application_count = DomainApplication.objects.filter(
|
||||
creator=self.request.user, status=DomainApplication.APPROVED
|
||||
creator=self.request.user, status=DomainApplication.ApplicationStatus.APPROVED
|
||||
).count()
|
||||
return approved_application_count > 0
|
||||
|
||||
|
@ -308,11 +308,15 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
|
||||
def pending_applications(self):
|
||||
"""Returns a List of user's applications with one of the following states:
|
||||
SUBMITTED, IN_REVIEW, ACTION_NEEDED"""
|
||||
# if the current application has ACTION_NEEDED status, this check should not be performed
|
||||
if self.application.status == DomainApplication.ACTION_NEEDED:
|
||||
ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED"""
|
||||
# if the current application has ApplicationStatus.ACTION_NEEDED status, this check should not be performed
|
||||
if self.application.status == DomainApplication.ApplicationStatus.ACTION_NEEDED:
|
||||
return []
|
||||
check_statuses = [DomainApplication.SUBMITTED, DomainApplication.IN_REVIEW, DomainApplication.ACTION_NEEDED]
|
||||
check_statuses = [
|
||||
DomainApplication.ApplicationStatus.SUBMITTED,
|
||||
DomainApplication.ApplicationStatus.IN_REVIEW,
|
||||
DomainApplication.ApplicationStatus.ACTION_NEEDED,
|
||||
]
|
||||
return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses)
|
||||
|
||||
def get_context_data(self):
|
||||
|
|
|
@ -280,6 +280,7 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
form.fields["server"].required = True
|
||||
else:
|
||||
form.fields["server"].required = False
|
||||
form.fields["server"].label += " (optional)"
|
||||
form.fields["domain"].initial = self.object.name
|
||||
return formset
|
||||
|
||||
|
@ -433,7 +434,7 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
return initial_data
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the DS Data page for the domain."""
|
||||
"""Redirect to the DS data page for the domain."""
|
||||
return reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -472,7 +473,7 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="disable-override-click">Remove all DS Data</button>'
|
||||
'name="disable-override-click">Remove all DS data</button>'
|
||||
)
|
||||
|
||||
# context to back out of a broken form on all fields delete
|
||||
|
@ -522,7 +523,7 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
logger.error(f"Registry error: {err}")
|
||||
return self.form_invalid(formset)
|
||||
else:
|
||||
messages.success(self.request, "The DS Data records for this domain have been updated.")
|
||||
messages.success(self.request, "The DS data records for this domain have been updated.")
|
||||
# superclass has the redirect
|
||||
return super().form_valid(formset)
|
||||
|
||||
|
@ -643,7 +644,46 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
"""Get an absolute URL for this domain."""
|
||||
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
||||
|
||||
def _make_invitation(self, email_address):
|
||||
def _send_domain_invitation_email(self, email: str, add_success=True):
|
||||
"""Performs the sending of the domain invitation email,
|
||||
does not make a domain information object
|
||||
email: string- email to send to
|
||||
add_success: bool- default True indicates:
|
||||
adding a success message to the view if the email sending succeeds"""
|
||||
# created a new invitation in the database, so send an email
|
||||
domainInfoResults = DomainInformation.objects.filter(domain=self.object)
|
||||
domainInfo = domainInfoResults.first()
|
||||
first = ""
|
||||
last = ""
|
||||
if domainInfo is not None:
|
||||
first = domainInfo.creator.first_name
|
||||
last = domainInfo.creator.last_name
|
||||
full_name = f"{first} {last}"
|
||||
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/domain_invitation.txt",
|
||||
"emails/domain_invitation_subject.txt",
|
||||
to_address=email,
|
||||
context={
|
||||
"domain_url": self._domain_abs_url(),
|
||||
"domain": self.object,
|
||||
"full_name": full_name,
|
||||
},
|
||||
)
|
||||
except EmailSendingError:
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
logger.warn(
|
||||
"Could not sent email invitation to %s for domain %s",
|
||||
email,
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
if add_success:
|
||||
messages.success(self.request, f"Invited {email} to this domain.")
|
||||
|
||||
def _make_invitation(self, email_address: str):
|
||||
"""Make a Domain invitation for this email and redirect with a message."""
|
||||
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
if not created:
|
||||
|
@ -653,34 +693,7 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
f"{email_address} has already been invited to this domain.",
|
||||
)
|
||||
else:
|
||||
# created a new invitation in the database, so send an email
|
||||
domaininfo = DomainInformation.objects.filter(domain=self.object)
|
||||
first = domaininfo.first().creator.first_name
|
||||
last = domaininfo.first().creator.last_name
|
||||
full_name = f"{first} {last}"
|
||||
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/domain_invitation.txt",
|
||||
"emails/domain_invitation_subject.txt",
|
||||
to_address=email_address,
|
||||
context={
|
||||
"domain_url": self._domain_abs_url(),
|
||||
"domain": self.object,
|
||||
"full_name": full_name,
|
||||
},
|
||||
)
|
||||
except EmailSendingError:
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
logger.warn(
|
||||
"Could not sent email invitation to %s for domain %s",
|
||||
email_address,
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
messages.success(self.request, f"Invited {email_address} to this domain.")
|
||||
|
||||
self._send_domain_invitation_email(email=email_address)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -692,6 +705,9 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
except User.DoesNotExist:
|
||||
# no matching user, go make an invitation
|
||||
return self._make_invitation(requested_email)
|
||||
else:
|
||||
# if user already exists then just send an email
|
||||
self._send_domain_invitation_email(requested_email, add_success=False)
|
||||
|
||||
try:
|
||||
UserDomainRole.objects.create(
|
||||
|
|
|
@ -101,10 +101,10 @@ class DomainPermission(PermissionsLoginMixin):
|
|||
|
||||
# Analysts may manage domains, when they are in these statuses:
|
||||
valid_domain_statuses = [
|
||||
DomainApplication.APPROVED,
|
||||
DomainApplication.IN_REVIEW,
|
||||
DomainApplication.REJECTED,
|
||||
DomainApplication.ACTION_NEEDED,
|
||||
DomainApplication.ApplicationStatus.APPROVED,
|
||||
DomainApplication.ApplicationStatus.IN_REVIEW,
|
||||
DomainApplication.ApplicationStatus.REJECTED,
|
||||
DomainApplication.ApplicationStatus.ACTION_NEEDED,
|
||||
# Edge case - some domains do not have
|
||||
# a status or DomainInformation... aka a status of 'None'.
|
||||
# It is necessary to access those to correct errors.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue