Merge branch 'main' into za/1816-domain-metadata-includes-organization-type

This commit is contained in:
zandercymatics 2024-04-17 14:58:05 -06:00
commit 9aa7235105
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
30 changed files with 989 additions and 152 deletions

View file

@ -0,0 +1,62 @@
---
name: Designer Onboarding
about: Onboarding steps for designers.
title: 'Designer Onboarding: GH_HANDLE'
labels: design, onboarding
assignees: katherineosos
---
# Designer Onboarding
- Onboardee: _GH handle of person being onboarded_
- Onboarder: _GH handle of onboard buddy_
Welcome to the .gov team! We're excited to have you here. Please follow the steps below to get everything set up. An onboarding buddy will help grant you access to all the tools and platforms we use. If you haven't been assigned an onboarding buddy, let us know in the #dotgov-disco channel.
## Onboardee
### Steps for the onboardee
- [ ] Read the [.gov onboarding doc](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?usp=sharing) thoroughly.
- [ ] Accept an invitation to our Slack workspace and fill out your profile.
- [ ] For our Slack profile names, we usually follow the naming convention of `Firstname Lastname (Org, State, pronouns)`.
Example: Katherine Osos (Truss, MN, she/her)
- [ ] Make sure you have been added to the necessary [channels](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.li3lqcygw8ax) and familiarize yourself with them.
- [ ] FOR FEDERAL EMPLOYEES: Create a [Google workspace enterprise account](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.xowzg9w0qlis) for CISA.
- [ ] Get access to our [Project Folder](https://drive.google.com/drive/folders/1qkoFQBlzXA7axi9CZ_OBhlJqRcqlNfpW?usp=drive_link) on Google Drive.
- [ ] Explore the folders and docs. Designers interface with the Product Design, Content, and Research folders most often.
- [ ] Make sure you have been invited to our [team meetings](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.emgtp2hgvabr) on Google Meet.
- [ ] Review our [design tools](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.aprurp3z4gmv).
- [ ] Accept invitation to our [Figma workspace](https://www.figma.com/files/1287135731043703282/team/1299882813146449644).
- [ ] Follow the steps in [preparing for your sandbox](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.au66hq5e0l8s) section on the onboarding doc.
- [ ] Schedule coffee chats with Design Lead, other designers, scrum master, and product manager ([team directory](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.1vq6r8e52e9f)).
- [ ] Look over [recommended reading](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.7ox9ee7v5q5n) and [relevant links](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.d9pac1gc751t).
- [ ] Fill out your own Personal Operating Manual (POM) on the [team norming board](https://miro.com/app/board/uXjVMxMu1SA=/). OPTIONAL: Present it on the next team coffee meeting.
- [ ] FOR FEDERAL EMPLOYEES: Check in with your manager on the CISA onboarding process and getting your PIV card.
- [ ] FOR CONTRACTORS: Check with your manager on your EOD clearance process.
- [ ] OPTIONAL: Request access to our [analytics tools](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.9q334hs4lbks).
- [ ] OPTIONAL: If you would like to manage content updates by running code locally rather than through GitHub, follow the steps in the [dev onboarding ticket](https://github.com/cisagov/getgov/issues/new?assignees=loganmeetsworld&labels=dev%2C+onboarding&template=developer-onboarding.md&title=Developer+Onboarding%3A+GH_HANDLE).
- [ ] If you would like to run code locally on a CISA laptop, reference the "[Setting up a developer environment on CISA laptops](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.2ctyba51d1zp)" section of the onboarding doc.
### Access
By following the steps, you should have access / been added to the following:
- [ ] The [.gov team](https://github.com/orgs/cisagov/teams/gov) under cisagov on GitHub
- [ ] [Slack](https://dhscisa.enterprise.slack.com), and have been added to the necessary channels
- [ ] [Google Drive Project folder](https://drive.google.com/drive/folders/1qkoFQBlzXA7axi9CZ_OBhlJqRcqlNfpW?usp=drive_link)
- [ ] [.gov team on Figma](https://www.figma.com/files/1287135731043703282/team/1299882813146449644) (as an editor if you have a license)
- [ ] [Team meetings](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.h62kzew057p1)
## Onboarder
### Steps for the onboarder
- [ ] Make sure onboardee is given an invitation to our Slack workspace
- [ ] Add onboardee to dotgov Slack channels
- [ ] Add onboardee to Project Folder as a "Contributor" (or if you do not have permissions, confirm with Cameron)
- If onboardee if a federal employee, add them as a "Content Manager" instead
- [ ] Coordinate with Cameron to grant onboardee a Figma license
- [ ] Add onboardee as an editor to the Figma team workspace, If they do not have a license (yet), you can add them as a viewer.
- [ ] Add onboardee to our team meetings
- [ ] If applicable, invite onboardee to Google Analytics, Google Search Console, and Search.gov console

View file

@ -4,7 +4,7 @@ The .gov domain helps U.S.-based government organizations gain public trust by b
## Onboarding
For new members of the @cisagov/dotgov team looking to contribute to the registrar, please open an [onboarding ticket](https://github.com/cisagov/getgov/issues/new?assignees=loganmeetsworld&labels=dev%2C+onboarding&template=developer-onboarding.md&title=Developer+Onboarding%3A+GH_HANDLE).
For new members of the @cisagov/dotgov team looking to contribute to the registrar, please open a [developer onboarding ticket](https://github.com/cisagov/getgov/issues/new?assignees=loganmeetsworld&labels=dev%2C+onboarding&template=developer-onboarding.md&title=Developer+Onboarding%3A+GH_HANDLE) or a [designer onboarding ticket](https://github.com/cisagov/getgov/issues/new?assignees=loganmeetsworld&labels=design%2C+onboarding&template=designer-onboarding.md&title=Designer+Onboarding%3A+GH_HANDLE).
## Code

View file

@ -73,14 +73,23 @@ cp ./.env-example .env
Get the secrets from Cloud.gov by running `cf env getgov-YOURSANDBOX`. More information is available in [rotate_application_secrets.md](../operations/runbooks/rotate_application_secrets.md).
## Adding user to /admin
## Getting access to /admin on all development sandboxes (also referred to as "adding to fixtures")
The endpoint /admin can be used to view and manage site content, including but not limited to user information and the list of current applications in the database. To be able to view and use /admin locally:
The endpoint /admin can be used to view and manage site content, including but not limited to user information and the list of current applications in the database. However, access to this is limited to analysts and full-access users with regular domain requestors and domain managers not being able to see this page.
While on production (the sandbox referred to as `stable`), an existing analyst or full-access user typically grants access /admin as part of onboarding ([see these instructions](../django-admin/roles.md)), doing this for all development sandboxes is very time consuming. Instead, to get access to /admin on all development sandboxes and when developing code locally, refer to the following sections depending on what level of user access you desire.
### Adding full-access user to /admin
To get access to /admin on every non-production sandbox and to use /admin in local development, do the following:
1. Login via login.gov
2. Go to the home page and make sure you can see the part where you can submit a domain request
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4
4. in src/registrar/fixtures_users.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below:
3. Go to /admin and it will tell you that your UUID is not authorized (it shows a very long string, this is your UUID). Copy that UUID for use in 4.
4. (Designers) Message in #getgov-dev that you need access to admin as a `superuser` and send them this UUID along with your desired email address. Please see the "Adding an Analyst to /admin" section below to complete similiar steps if you also desire an `analyst` user account. Engineers will handle the remaining steps for designers, stop here.
(Engineers) In src/registrar/fixtures_users.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below:
```
ADMINS = [
@ -93,16 +102,18 @@ The endpoint /admin can be used to view and manage site content, including but n
]
```
5. In the browser, navigate to /admin. To verify that all is working correctly, under "domain requests" you should see fake domains with various fake statuses.
6. Add an optional email key/value pair
5. (Engineers) In the browser, navigate to /admin. To verify that all is working correctly, under "domain requests" you should see fake domains with various fake statuses.
6. (Engineers) Add an optional email key/value pair
### Adding an Analyst to /admin
### Adding an analyst-level user to /admin
Analysts are a variant of the admin role with limited permissions. The process for adding an Analyst is much the same as adding an admin:
1. Login via login.gov (if you already exist as an admin, you will need to create a separate login.gov account for this: i.e. first.last+1@email.com)
2. Go to the home page and make sure you can see the part where you can submit a domain request
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 (this will be a different UUID than the one obtained from creating an admin)
4. in src/registrar/fixtures_users.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below:
4. (Designers) Message in #getgov-dev that you need access to admin as a `superuser` and send them this UUID along with your desired email address. Engineers will handle the remaining steps for designers, stop here.
5. (Engineers) In src/registrar/fixtures_users.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below:
```
STAFF = [
@ -115,10 +126,11 @@ Analysts are a variant of the admin role with limited permissions. The process f
]
```
5. In the browser, navigate to /admin. To verify that all is working correctly, verify that you can only see a sub-section of the modules and some are set to view-only.
6. Add an optional email key/value pair
5. (Engineers) In the browser, navigate to /admin. To verify that all is working correctly, verify that you can only see a sub-section of the modules and some are set to view-only.
6. (Engineers) Add an optional email key/value pair
Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name, or use a completely different first/last name to avoid confusion. Example: `Bob-Analyst`
## Adding to CODEOWNERS (optional)
The CODEOWNERS file sets the tagged individuals as default reviewers on any Pull Request that changes files that they are marked as owners of.

View file

@ -56,6 +56,13 @@ cf ssh getgov-ENVIRONMENT
./manage.py dumpdata
```
## Access certain table in the database
1. `cf connect-to-service getgov-ENVIRONMENT getgov-ENVIRONMENT-database` gets you into whichever environments database you'd like
2. `\c [table name here that starts cgaws...etc];` connects to the [cgaws...etc] table
3. `\dt` retrieves information about that table and displays it
4. Make sure the table you are looking for exists. For this example, we are looking for `django_migrations`
5. Run `SELECT * FROM django_migrations;` to see everything that's in it!
## Dropping and re-creating the database
For your sandbox environment, it might be necessary to start the database over from scratch.

View file

@ -121,3 +121,19 @@ https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1697810600723069
2. `./manage.py migrate model_name_here file_name_WITH_create` (run the last data creation migration AND ONLY THAT ONE)
3. `./manage.py migrate --fake model_name_here most_recent_file_name` (fake migrate the last migration in the migration list)
4. `./manage.py load` (rerun fixtures)
### Scenario 9: Inconsistent Migration History
If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error, or when you run `./manage.py showmigrations` it looks like:
[x] 0056_example_migration
[ ] 0057_other_migration
[x] 0058_some_other_migration
1. Go to [database-access.md](../database-access.md#access-certain-table-in-the-database) to see the commands on how to access a certain table in the database.
2. In this case, we want to remove the migration "history" from the `django_migrations` table
3. Once you are in the `cgaws...` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
4. Find the id of the "history" you want to delete. This will be the one in the far left column. For this example, let's pretend the id is 101.
5. Run `DELETE FROM django_migrations WHERE id=101;` where 101 is an example id as seen above.
6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state. Most likely you will see several unapplied migrations.
7. If you still have unapplied migrations, run `./manage.py migrate`. If an error occurs saying one has already been applied, fake that particular migration `./manage.py migrate --fake model_name_here migration_number` and then run the normal `./manage.py migrate` command to then apply those migrations that come after the one that threw the error.

View file

@ -9,7 +9,7 @@ our `user_group` model and run in a migration.
For more details, refer to the [user group model](../../src/registrar/models/user_group.py).
## Adding a user as analyst or granting full access via django-admin
## Adding a user as analyst or granting full access via django-admin (/admin)
If a new team member has joined, then they will need to be granted analyst (`cisa_analysts_group`) or full access (`full_access_group`) permissions in order to view the admin pages. These admin pages are the ones found at manage.get.gov/admin.
To do this, do the following:
@ -21,7 +21,7 @@ To do this, do the following:
5. (Optional) If the user needs access to django admin (such as an analyst), then you will also need to make sure "Staff Status" is checked. This can be found in the same `User Permissions` section right below the checkbox for `Active`.
6. Click `Save` to apply all changes.
## Removing a user group permission via django-admin
## Removing a user group permission via django-admin (/admin)
If an employee was given the wrong permissions or has had a change in roles that subsequently requires a permission change, then their permissions should be updated in django-admin. Much like in the previous section you can accomplish this by doing the following:

View file

@ -588,14 +588,23 @@ Example: `cf ssh getgov-za`
| 1 | **debug** | Increases logging detail. Defaults to False. |
## Populate First Ready
This section outlines how to run the `populate_organization_type` script.
## Populate Organization type
This section outlines how to run the `populate_organization_type` script.
The script is used to update the organization_type field on DomainRequest and DomainInformation when it is None.
That data are synthesized from the generic_org_type field and the is_election_board field by concatenating " - Elections" on the end of generic_org_type string if is_elections_board is True.
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: Get the domain_election_board file
The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
After downloading this file, place it in `src/migrationdata`
#### Step 2: Upload the domain_election_board file to your sandbox
Follow [Step 1: Transfer data to sandboxes](#step-1-transfer-data-to-sandboxes) and [Step 2: Transfer uploaded files to the getgov directory](#step-2-transfer-uploaded-files-to-the-getgov-directory) from the [Set Up Migrations on Sandbox](#set-up-migrations-on-sandbox) portion of this doc.
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
@ -605,28 +614,31 @@ Example: `cf ssh getgov-za`
```/tmp/lifecycle/shell```
#### Step 4: Running the script
```./manage.py populate_organization_type {domain_election_office_filename} --debug```
```./manage.py populate_organization_type {domain_election_board_filename}```
- The domain_election_office_filename file must adhere to this format:
- The domain_election_board_filename file must adhere to this format:
- example.gov\
example2.gov\
example3.gov
Example:
`./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
`./manage.py populate_organization_type migrationdata/election-domains.csv`
### Running locally
```docker-compose exec app ./manage.py populate_organization_type {domain_election_office_filename} --debug```
#### Step 1: Get the domain_election_board file
The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
After downloading this file, place it in `src/migrationdata`
#### Step 2: Running the script
```docker-compose exec app ./manage.py populate_organization_type {domain_election_board_filename}```
Example (assuming that this is being ran from src/):
`docker-compose exec app ./manage.py populate_organization_type migrationdata/election-domains.csv --debug`
`docker-compose exec app ./manage.py populate_organization_type migrationdata/election-domains.csv`
##### Required parameters
### Required parameters
| | Parameter | Description |
|:-:|:------------------------------------|:-------------------------------------------------------------------|
| 1 | **domain_election_office_filename** | A file containing every domain that is an election office.
##### Optional parameters
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **debug** | Increases logging detail. Defaults to False.
| 1 | **domain_election_board_filename** | A file containing every domain that is an election office.

View file

@ -663,6 +663,7 @@ class ContactAdmin(ListHeaderAdmin):
list_display = [
"contact",
"email",
"user_exists",
]
# this ordering effects the ordering of results
# in autocomplete_fields for user
@ -679,6 +680,13 @@ class ContactAdmin(ListHeaderAdmin):
change_form_template = "django/admin/email_clipboard_change_form.html"
def user_exists(self, obj):
"""Check if the Contact has a related User"""
return "Yes" if obj.user is not None else "No"
user_exists.short_description = "Is user" # type: ignore
user_exists.admin_order_field = "user" # type: ignore
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
@ -779,6 +787,46 @@ class WebsiteAdmin(ListHeaderAdmin):
]
search_help_text = "Search by website."
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return {}
return super().get_model_perms(request)
def has_change_permission(self, request, obj=None):
"""
Allow analysts to access the change form directly via URL.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return True
return super().has_change_permission(request, obj)
def response_change(self, request, obj):
"""
Override to redirect users back to the previous page after saving.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
return_path = request.GET.get("return_path")
# First, call the super method to perform the standard operations and capture the response
response = super().response_change(request, obj)
# Don't redirect to the website page on save if the user is an analyst.
# Rather, just redirect back to the originating page.
if (analyst_perm and not superuser_perm) and return_path:
# Redirect to the return path if it exists
return HttpResponseRedirect(return_path)
# If no redirection is needed, return the original response
return response
class UserDomainRoleAdmin(ListHeaderAdmin):
"""Custom user domain role admin class."""
@ -1395,12 +1443,36 @@ class DomainRequestAdmin(ListHeaderAdmin):
"""
Override changelist_view to set the selected value of status filter.
"""
# there are two conditions which should set the default selected filter:
# 1 - there are no query parameters in the request and the request is the
# initial request for this view
# 2 - there are no query parameters in the request and the referring url is
# the change view for a domain request
should_apply_default_filter = False
# use http_referer in order to distinguish between request as a link from another page
# and request as a removal of all filters
http_referer = request.META.get("HTTP_REFERER", "")
# if there are no query parameters in the request
# and the request is the initial request for this view
if not bool(request.GET) and request.path not in http_referer:
if not bool(request.GET):
# if the request is the initial request for this view
if request.path not in http_referer:
should_apply_default_filter = True
# elif the request is a referral from changelist view or from
# domain request change view
elif request.path in http_referer:
# find the index to determine the referring url after the path
index = http_referer.find(request.path)
# Check if there is a character following the path in http_referer
next_char_index = index + len(request.path)
if index + next_char_index < len(http_referer):
next_char = http_referer[next_char_index]
# Check if the next character is a digit, if so, this indicates
# a change view for domain request
if next_char.isdigit():
should_apply_default_filter = True
if should_apply_default_filter:
# modify the GET of the request to set the selected filter
modified_get = copy.deepcopy(request.GET)
modified_get["status__in"] = "submitted,in review,action needed"
@ -1466,7 +1538,10 @@ class DomainInformationInline(admin.StackedInline):
def has_change_permission(self, request, obj=None):
"""Custom has_change_permission override so that we can specify that
analysts can edit this through this inline, but not through the model normally"""
if request.user.has_perm("registrar.analyst_access_permission"):
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return True
return super().has_change_permission(request, obj)
@ -1627,12 +1702,8 @@ class DomainAdmin(ListHeaderAdmin):
# No expiration date was found. Return none.
extra_context["extended_expiration_date"] = None
return super().changeform_view(request, object_id, form_url, extra_context)
if curr_exp_date < date.today():
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
else:
new_date = domain.registry_expiration_date + relativedelta(years=years_to_extend_by)
extra_context["extended_expiration_date"] = new_date
new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
extra_context["extended_expiration_date"] = new_date
else:
extra_context["extended_expiration_date"] = None
@ -1896,6 +1967,46 @@ class DraftDomainAdmin(ListHeaderAdmin):
# in autocomplete_fields for user
ordering = ["name"]
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return {}
return super().get_model_perms(request)
def has_change_permission(self, request, obj=None):
"""
Allow analysts to access the change form directly via URL.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return True
return super().has_change_permission(request, obj)
def response_change(self, request, obj):
"""
Override to redirect users back to the previous page after saving.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
return_path = request.GET.get("return_path")
# First, call the super method to perform the standard operations and capture the response
response = super().response_change(request, obj)
# Don't redirect to the website page on save if the user is an analyst.
# Rather, just redirect back to the originating page.
if (analyst_perm and not superuser_perm) and return_path:
# Redirect to the return path if it exists
return HttpResponseRedirect(return_path)
# If no redirection is needed, return the original response
return response
class PublicContactAdmin(ListHeaderAdmin):
"""Custom PublicContact admin class."""

View file

@ -530,7 +530,7 @@ function hideDeletedForms() {
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
// The Nameservers formset features 2 required and 11 optionals
if (isNameserversForm) {
cloneIndex = 2;
// cloneIndex = 2;
formLabel = "Name server";
// DNSSEC: DS Data
} else if (isDsDataForm) {
@ -766,3 +766,21 @@ function toggleTwoDomElements(ele1, ele2, index) {
}
})();
/**
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
*
*/
(function nameserversFormListener() {
let isNameserversForm = document.querySelector(".nameservers-form");
if (isNameserversForm) {
let forms = document.querySelectorAll(".repeatable-form");
if (forms.length < 3) {
// Hide the delete buttons on the 2 nameservers
forms.forEach((form) => {
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
});
}
}
})();

View file

@ -495,6 +495,8 @@ address.dja-address-contact-list {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: medium;
padding-top: 3px !important;
}
}
@ -505,6 +507,7 @@ address.dja-address-contact-list {
@media screen and (min-width:768px) {
.visible-768 {
display: block;
padding-top: 0;
}
}
@ -536,6 +539,18 @@ address.dja-address-contact-list {
}
}
.dja-status-list {
border-top: solid 1px var(--border-color);
margin-left: 0 !important;
padding-left: 0 !important;
padding-top: 10px;
li {
line-height: 1.5;
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif !important;
padding-top: 0;
padding-bottom: 0;
}
}
// Make the clipboard button "float" inside of the input box
.admin-icon-group {

View file

@ -83,25 +83,34 @@ class DomainNameserverForm(forms.Form):
# after clean_fields. it is used to determine form level errors.
# is_valid is typically called from view during a post
cleaned_data = super().clean()
self.clean_empty_strings(cleaned_data)
server = cleaned_data.get("server", "")
# remove ANY spaces in the server field
server = server.replace(" ", "")
# lowercase the server
server = server.lower()
server = server.replace(" ", "").lower()
cleaned_data["server"] = server
ip = cleaned_data.get("ip", None)
# remove ANY spaces in the ip field
ip = cleaned_data.get("ip", "")
ip = ip.replace(" ", "")
cleaned_data["ip"] = ip
domain = cleaned_data.get("domain", "")
ip_list = self.extract_ip_list(ip)
# validate if the form has a server or an ip
# Capture the server_value
server_value = self.cleaned_data.get("server")
# Validate if the form has a server or an ip
if (ip and ip_list) or server:
self.validate_nameserver_ip_combo(domain, server, ip_list)
# Re-set the server value:
# add_error which is called on validate_nameserver_ip_combo will clean-up (delete) any invalid data.
# We need that data because we need to know the total server entries (even if invalid) in the formset
# clean method where we determine whether a blank first and/or second entry should throw a required error.
self.cleaned_data["server"] = server_value
return cleaned_data
def clean_empty_strings(self, cleaned_data):
@ -149,6 +158,19 @@ class BaseNameserverFormset(forms.BaseFormSet):
"""
Check for duplicate entries in the formset.
"""
# Check if there are at least two valid servers
valid_servers_count = sum(
1 for form in self.forms if form.cleaned_data.get("server") and form.cleaned_data.get("server").strip()
)
if valid_servers_count >= 2:
# If there are, remove the "At least two name servers are required" error from each form
# This will allow for successful submissions when the first or second entries are blanked
# but there are enough entries total
for form in self.forms:
if form.errors.get("server") == ["At least two name servers are required."]:
form.errors.pop("server")
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
@ -156,10 +178,13 @@ class BaseNameserverFormset(forms.BaseFormSet):
data = []
duplicates = []
for form in self.forms:
for index, form in enumerate(self.forms):
if form.cleaned_data:
value = form.cleaned_data["server"]
if value in data:
# We need to make sure not to trigger the duplicate error in case the first and second nameservers
# are empty. If there are enough records in the formset, that error is an unecessary blocker.
# If there aren't, the required error will block the submit.
if value in data and not (form.cleaned_data.get("server", "").strip() == "" and index == 1):
form.add_error(
"server",
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),

View file

@ -11,7 +11,11 @@ logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Loops through each valid DomainInformation and DomainRequest object and updates its organization_type value"
help = (
"Loops through each valid DomainInformation and DomainRequest object and updates its organization_type value. "
"A valid DomainInformation/DomainRequest in this sense is one that has the value None for organization_type. "
"In other words, we populate the organization_type field if it is not already populated."
)
def __init__(self):
super().__init__()
@ -26,34 +30,26 @@ class Command(BaseCommand):
self.di_skipped: List[DomainInformation] = []
# Define a global variable for all domains with election offices
self.domains_with_election_offices_set = set()
self.domains_with_election_boards_set = set()
def add_arguments(self, parser):
"""Adds command line arguments"""
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument(
"domain_election_office_filename",
"domain_election_board_filename",
help=("A file that contains" " all the domains that are election offices."),
)
def handle(self, domain_election_office_filename, **kwargs):
def handle(self, domain_election_board_filename, **kwargs):
"""Loops through each valid Domain object and updates its first_created value"""
debug = kwargs.get("debug")
# Check if the provided file path is valid
if not os.path.isfile(domain_election_office_filename):
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_office_filename}'")
if not os.path.isfile(domain_election_board_filename):
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_board_filename}'")
with open(domain_election_office_filename, "r") as file:
for line in file:
# Remove any leading/trailing whitespace
domain = line.strip()
if domain not in self.domains_with_election_offices_set:
self.domains_with_election_offices_set.add(domain)
# Read the election office csv
self.read_election_board_file(domain_election_board_filename)
domain_requests = DomainRequest.objects.filter(
organization_type__isnull=True, requested_domain__name__isnull=False
)
domain_requests = DomainRequest.objects.filter(organization_type__isnull=True)
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
@ -68,7 +64,7 @@ class Command(BaseCommand):
)
logger.info("Updating DomainRequest(s)...")
self.update_domain_requests(domain_requests, debug)
self.update_domain_requests(domain_requests)
# We should actually be targeting all fields with no value for organization type,
# but do have a value for generic_org_type. This is because there is data that we can infer.
@ -86,23 +82,55 @@ class Command(BaseCommand):
)
logger.info("Updating DomainInformation(s)...")
self.update_domain_informations(domain_infos, debug)
self.update_domain_informations(domain_infos)
def update_domain_requests(self, domain_requests, debug):
def read_election_board_file(self, domain_election_board_filename):
"""
Reads the election board file and adds each parsed domain to self.domains_with_election_boards_set.
As previously implied, this file contains information about Domains which have election boards.
The file must adhere to this format:
```
domain1.gov
domain2.gov
domain3.gov
```
(and so on)
"""
with open(domain_election_board_filename, "r") as file:
for line in file:
# Remove any leading/trailing whitespace
domain = line.strip()
if domain not in self.domains_with_election_boards_set:
self.domains_with_election_boards_set.add(domain)
def update_domain_requests(self, domain_requests):
"""
Updates the organization_type for a list of DomainRequest objects using the `sync_organization_type` function.
Results are then logged.
This function updates the following variables:
- self.request_to_update list is appended to if the field was updated successfully.
- self.request_skipped list is appended to if the field has `None` for `request.generic_org_type`.
- self.request_failed_to_update list is appended to if an exception is caught during update.
"""
for request in domain_requests:
try:
if request.generic_org_type is not None:
domain_name = request.requested_domain.name
request.is_election_board = domain_name in self.domains_with_election_offices_set
request = self.sync_organization_type(DomainRequest, request)
self.request_to_update.append(request)
domain_name = None
if request.requested_domain is not None and request.requested_domain.name is not None:
domain_name = request.requested_domain.name
if debug:
logger.info(f"Updating {request} => {request.organization_type}")
request_is_approved = request.status == DomainRequest.DomainRequestStatus.APPROVED
if request_is_approved and domain_name is not None and not request.is_election_board:
request.is_election_board = domain_name in self.domains_with_election_boards_set
self.sync_organization_type(DomainRequest, request)
self.request_to_update.append(request)
logger.info(f"Updating {request} => {request.organization_type}")
else:
self.request_skipped.append(request)
if debug:
logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
except Exception as err:
self.request_failed_to_update.append(request)
logger.error(err)
@ -116,23 +144,44 @@ class Command(BaseCommand):
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAINREQUEST ==============="
TerminalHelper.log_script_run_summary(
self.request_to_update, self.request_failed_to_update, self.request_skipped, debug, log_header
self.request_to_update, self.request_failed_to_update, self.request_skipped, True, log_header
)
def update_domain_informations(self, domain_informations, debug):
update_skipped_count = len(self.request_to_update)
if update_skipped_count > 0:
logger.warning(
f"""{TerminalColors.MAGENTA}
Note: Entries are skipped when generic_org_type is None
{TerminalColors.ENDC}
"""
)
def update_domain_informations(self, domain_informations):
"""
Updates the organization_type for a list of DomainInformation objects
and updates is_election_board if the domain is in the provided csv.
Results are then logged.
This function updates the following variables:
- self.di_to_update list is appended to if the field was updated successfully.
- self.di_skipped list is appended to if the field has `None` for `request.generic_org_type`.
- self.di_failed_to_update list is appended to if an exception is caught during update.
"""
for info in domain_informations:
try:
if info.generic_org_type is not None:
domain_name = info.domain.name
info.is_election_board = domain_name in self.domains_with_election_offices_set
info = self.sync_organization_type(DomainInformation, info)
if not info.is_election_board:
info.is_election_board = domain_name in self.domains_with_election_boards_set
self.sync_organization_type(DomainInformation, info)
self.di_to_update.append(info)
if debug:
logger.info(f"Updating {info} => {info.organization_type}")
logger.info(f"Updating {info} => {info.organization_type}")
else:
self.di_skipped.append(info)
if debug:
logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
except Exception as err:
self.di_failed_to_update.append(info)
logger.error(err)
@ -146,9 +195,18 @@ class Command(BaseCommand):
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAININFORMATION ==============="
TerminalHelper.log_script_run_summary(
self.di_to_update, self.di_failed_to_update, self.di_skipped, debug, log_header
self.di_to_update, self.di_failed_to_update, self.di_skipped, True, log_header
)
update_skipped_count = len(self.di_skipped)
if update_skipped_count > 0:
logger.warning(
f"""{TerminalColors.MAGENTA}
Note: Entries are skipped when generic_org_type is None
{TerminalColors.ENDC}
"""
)
def sync_organization_type(self, sender, instance):
"""
Updates the organization_type (without saving) to match
@ -159,7 +217,7 @@ class Command(BaseCommand):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()
@ -168,7 +226,7 @@ class Command(BaseCommand):
election_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_election_to_org_generic()
# Manages the "organization_type" variable and keeps in sync with
# "is_election_office" and "generic_organization_type"
# "is_election_board" and "generic_organization_type"
org_type_helper = CreateOrUpdateOrganizationTypeHelper(
sender=sender,
instance=instance,
@ -176,5 +234,4 @@ class Command(BaseCommand):
election_org_to_generic_org_map=election_org_map,
)
instance = org_type_helper.create_or_update_organization_type()
return instance
org_type_helper.create_or_update_organization_type(force_update=True)

View file

@ -0,0 +1,37 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
# It is dependent on 0079 (which populates federal agencies)
# 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", "0083_alter_contact_email_alter_publiccontact_email"),
]
operations = [
migrations.RunPython(
create_groups,
reverse_code=migrations.RunPython.noop,
atomic=True,
),
]

View file

@ -94,6 +94,9 @@ class Contact(TimeStampedModel):
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]
return " ".join(names) if names else "Unknown"
def has_contact_info(self):
return bool(self.title or self.email or self.phone)
def save(self, *args, **kwargs):
# Call the parent class's save method to perform the actual save
super().save(*args, **kwargs)

View file

@ -246,7 +246,7 @@ class DomainInformation(TimeStampedModel):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()

View file

@ -675,7 +675,7 @@ class DomainRequest(TimeStampedModel):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = self.OrgChoicesElectionOffice.get_org_generic_to_org_election()

View file

@ -9,6 +9,7 @@ from .domain_invitation import DomainInvitation
from .transition_domain import TransitionDomain
from .verified_by_staff import VerifiedByStaff
from .domain import Domain
from .domain_request import DomainRequest
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
@ -67,6 +68,33 @@ class User(AbstractUser):
def is_restricted(self):
return self.status == self.RESTRICTED
def get_approved_domains_count(self):
"""Return count of approved domains"""
allowed_states = [Domain.State.UNKNOWN, Domain.State.DNS_NEEDED, Domain.State.READY, Domain.State.ON_HOLD]
approved_domains_count = self.domains.filter(state__in=allowed_states).count()
return approved_domains_count
def get_active_requests_count(self):
"""Return count of active requests"""
allowed_states = [
DomainRequest.DomainRequestStatus.SUBMITTED,
DomainRequest.DomainRequestStatus.IN_REVIEW,
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
]
active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count()
return active_requests_count
def get_rejected_requests_count(self):
"""Return count of rejected requests"""
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count()
def get_ineligible_requests_count(self):
"""Return count of ineligible requests"""
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count()
def has_contact_info(self):
return bool(self.contact.title or self.contact.email or self.contact.phone)
@classmethod
def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification

View file

@ -41,11 +41,6 @@ class UserGroup(Group):
"model": "domain",
"permissions": ["view_domain"],
},
{
"app_label": "registrar",
"model": "draftdomain",
"permissions": ["change_draftdomain"],
},
{
"app_label": "registrar",
"model": "user",
@ -56,11 +51,6 @@ class UserGroup(Group):
"model": "domaininvitation",
"permissions": ["add_domaininvitation", "view_domaininvitation"],
},
{
"app_label": "registrar",
"model": "website",
"permissions": ["change_website"],
},
{
"app_label": "registrar",
"model": "userdomainrole",

View file

@ -49,7 +49,7 @@ class CreateOrUpdateOrganizationTypeHelper:
self.generic_org_to_org_map = generic_org_to_org_map
self.election_org_to_generic_org_map = election_org_to_generic_org_map
def create_or_update_organization_type(self):
def create_or_update_organization_type(self, force_update=False):
"""The organization_type field on DomainRequest and DomainInformation is consituted from the
generic_org_type and is_election_board fields. To keep the organization_type
field up to date, we need to update it before save based off of those field
@ -59,6 +59,14 @@ class CreateOrUpdateOrganizationTypeHelper:
one of the excluded types (FEDERAL, INTERSTATE, SCHOOL_DISTRICT), the
organization_type is set to a corresponding election variant. Otherwise, it directly
mirrors the generic_org_type value.
args:
force_update (bool): If an existing instance has no values to change,
try to update the organization_type field (or related fields) anyway.
This is done by invoking the new instance handler.
Use to force org type to be updated to the correct value even
if no other changes were made (including is_election).
"""
# A new record is added with organization_type not defined.
@ -67,7 +75,7 @@ class CreateOrUpdateOrganizationTypeHelper:
if is_new_instance:
self._handle_new_instance()
else:
self._handle_existing_instance()
self._handle_existing_instance(force_update)
return self.instance
@ -92,7 +100,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
def _handle_existing_instance(self):
def _handle_existing_instance(self, force_update_when_no_are_changes_found=False):
# == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = self.sender.objects.get(id=self.instance.id)
@ -109,17 +117,19 @@ class CreateOrUpdateOrganizationTypeHelper:
# This will not happen in normal flow as it is not possible otherwise.
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
# No values to update - do nothing
return None
# == Program flow will halt here if there is no reason to update == #
# No changes found
if force_update_when_no_are_changes_found:
# If we want to force an update anyway, we can treat this record like
# its a new one in that we check for "None" values rather than changes.
self._handle_new_instance()
else:
# == Update the linked values == #
# Find out which field needs updating
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
generic_org_type_needs_update = organization_type_changed
# == Update the linked values == #
# Find out which field needs updating
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
generic_org_type_needs_update = organization_type_changed
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
"""

View file

@ -116,8 +116,8 @@
</button>
</span>
<p class="text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
<strong>Requested domain:</strong> {{ original.requested_domain.name }}
<p class="padding-top-05 text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
Requested domain: <strong>{{ original.requested_domain.name }}</strong>
</p>
{{ block.super }}
</div>

View file

@ -1,6 +1,6 @@
{% load i18n static %}
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} dja-address-contact-list">
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user.has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list">
{% if show_formatted_name %}
{% if contact.get_formatted_name %}
@ -10,7 +10,7 @@
{% endif %}
{% endif %}
{% if user.title or user.contact.title or user.email or user.contact.email or user.phone or user.contact.phone %}
{% if user.has_contact_info %}
{# Title #}
{% if user.title or user.contact.title %}
{% if user.contact.title %}

View file

@ -27,6 +27,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
</dl>
</div>
{% endif %}
{% elif field.field.name == "requested_domain" %}
{% with current_path=request.get_full_path %}
<a class="margin-top-05 padding-top-05" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
{% endwith%}
{% elif field.field.name == "current_websites" %}
{% comment %}
The "website" model is essentially just a text field.
@ -49,9 +53,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
</div>
{% elif field.field.name == "alternative_domains" %}
<div class="readonly">
{% with current_path=request.get_full_path %}
{% for alt_domain in original.alternative_domains.all %}
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %}
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}?{{ 'return_path='|add:current_path }}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endwith %}
</div>
{% else %}
<div class="readonly">{{ field.contents }}</div>
@ -65,6 +71,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<label aria-label="Creator contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
</div>
<div class="flex-container">
<label aria-label="User summary details"></label>
{% include "django/admin/includes/user_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
</div>
{% elif field.field.name == "submitter" %}
<div class="flex-container">
<label aria-label="Submitter contact details"></label>

View file

@ -0,0 +1,30 @@
{% load i18n static %}
{% with approved_domains_count=user.get_approved_domains_count %}
{% with active_requests_count=user.get_active_requests_count %}
{% with rejected_requests_count=user.get_rejected_requests_count %}
{% with ineligible_requests_count=user.get_ineligible_requests_count %}
{% if approved_domains_count|add:active_requests_count|add:rejected_requests_count|add:ineligible_requests_count > 0 %}
<ul class="dja-status-list">
{% if approved_domains_count > 0 %}
{# Approved domains #}
<li>Approved domains: {{ approved_domains_count }}</li>
{% endif %}
{% if active_requests_count > 0 %}
{# Active requests #}
<li>Active requests: {{ active_requests_count }}</li>
{% endif %}
{% if rejected_requests_count > 0 %}
{# Rejected requests #}
<li>Rejected requests: {{ rejected_requests_count }}</li>
{% endif %}
{% if ineligible_requests_count > 0 %}
{# Ineligible requests #}
<li>Ineligible requests: {{ ineligible_requests_count }}</li>
{% endif %}
</ul>
{% endif %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}

View file

@ -3,7 +3,7 @@
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold" >
Next steps in this process
</h2>
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 20 business days. Well email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 30 business days. Well email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
Need to make changes?

View file

@ -1152,6 +1152,18 @@ class MockEppLib(TestCase):
],
)
infoDomainFourHosts = fakedEppObject(
"fournameserversDomain.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
contacts=[],
hosts=[
"ns1.my-nameserver-1.com",
"ns1.my-nameserver-2.com",
"ns1.cats-are-superior3.com",
"ns1.explosive-chicken-nuggets.com",
],
)
infoDomainNoHost = fakedEppObject(
"my-nameserver.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
@ -1452,7 +1464,9 @@ class MockEppLib(TestCase):
)
def mockInfoDomainCommands(self, _request, cleaned):
request_name = getattr(_request, "name", None)
request_name = getattr(_request, "name", None).lower()
print(request_name)
# Define a dictionary to map request names to data and extension values
request_mappings = {
@ -1474,7 +1488,8 @@ class MockEppLib(TestCase):
"nameserverwithip.gov": (self.infoDomainHasIP, None),
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
"threenameserversdomain.gov": (self.infoDomainThreeHosts, None),
"fournameserversdomain.gov": (self.infoDomainFourHosts, None),
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),

View file

@ -21,7 +21,16 @@ from registrar.admin import (
UserDomainRoleAdmin,
VerifiedByStaffAdmin,
)
from registrar.models import Domain, DomainRequest, DomainInformation, User, DomainInvitation, Contact, Website
from registrar.models import (
Domain,
DomainRequest,
DomainInformation,
User,
DomainInvitation,
Contact,
Website,
DraftDomain,
)
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.verified_by_staff import VerifiedByStaff
from .common import (
@ -76,11 +85,10 @@ class TestDomainAdmin(MockEppLib, WebTest):
)
super().setUp()
@skip("TODO for another ticket. This test case is grabbing old db data.")
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
def test_extend_expiration_date_button(self, mock_date_today):
"""
Tests if extend_expiration_date button extends correctly
Tests if extend_expiration_date modal gives an accurate date
"""
# Create a ready domain with a preset expiration date
@ -107,17 +115,11 @@ class TestDomainAdmin(MockEppLib, WebTest):
# Follow the response
response = response.follow()
# refresh_from_db() does not work for objects with protected=True.
# https://github.com/viewflow/django-fsm/issues/89
new_domain = Domain.objects.get(id=domain.id)
# Check that the current expiration date is what we expect
self.assertEqual(new_domain.expiration_date, date(2025, 5, 25))
# Assert that everything on the page looks correct
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date")
self.assertContains(response, "New expiration date: <b>May 25, 2025</b>")
# Ensure the message we recieve is in line with what we expect
expected_message = "Successfully extended the expiration date."
@ -129,6 +131,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
extra_tags="",
fail_silently=False,
)
mock_add_message.assert_has_calls([expected_call], 1)
@less_console_noise_decorator
@ -703,6 +706,126 @@ class TestDomainRequestAdmin(MockEppLib):
)
self.mock_client = MockSESClient()
@less_console_noise_decorator
def test_analyst_can_see_and_edit_alternative_domain(self):
"""Tests if an analyst can still see and edit the alternative domain field"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
fake_website = Website.objects.create(website="thisisatest.gov")
_domain_request.alternative_domains.add(fake_website)
_domain_request.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, _domain_request.requested_domain.name)
# Test if the page has the alternative domain
self.assertContains(response, "thisisatest.gov")
# Check that the page contains the url we expect
expected_href = reverse("admin:registrar_website_change", args=[fake_website.id])
self.assertContains(response, expected_href)
# Navigate to the website to ensure that we can still edit it
response = self.client.get(
"/admin/registrar/website/{}/change/".format(fake_website.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, "thisisatest.gov")
@less_console_noise_decorator
def test_analyst_can_see_and_edit_requested_domain(self):
"""Tests if an analyst can still see and edit the requested domain field"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
follow=True,
)
# Filter to get the latest from the DB (rather than direct assignment)
requested_domain = DraftDomain.objects.filter(name=_domain_request.requested_domain.name).get()
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, requested_domain.name)
# Check that the page contains the url we expect
expected_href = reverse("admin:registrar_draftdomain_change", args=[requested_domain.id])
self.assertContains(response, expected_href)
# Navigate to the website to ensure that we can still edit it
response = self.client.get(
"/admin/registrar/draftdomain/{}/change/".format(requested_domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, "city.gov")
@less_console_noise_decorator
def test_analyst_can_see_current_websites(self):
"""Tests if an analyst can still see current website field"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
fake_website = Website.objects.create(website="thisisatest.gov")
_domain_request.current_websites.add(fake_website)
_domain_request.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, _domain_request.requested_domain.name)
# Test if the page has the current website
self.assertContains(response, "thisisatest.gov")
def test_domain_sortable(self):
"""Tests if the DomainRequest sorts by domain correctly"""
with less_console_noise():
@ -1386,7 +1509,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
expected_content = "<strong>Requested domain:</strong>"
expected_content = "Requested domain:"
expected_content2 = '<span class="scroll-indicator"></span>'
expected_content3 = '<div class="submit-row-wrapper">'
not_expected_content = "submit-row-wrapper--analyst-view>"
@ -1415,7 +1538,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
expected_content = "<strong>Requested domain:</strong>"
expected_content = "Requested domain:"
expected_content2 = '<span class="scroll-indicator"></span>'
expected_content3 = '<div class="submit-row-wrapper submit-row-wrapper--analyst-view">'
self.assertContains(request, expected_content)
@ -1553,6 +1676,10 @@ class TestDomainRequestAdmin(MockEppLib):
# Test for the copy link
self.assertContains(response, "usa-button__clipboard", count=4)
# Test that Creator counts display properly
self.assertNotContains(response, "Approved domains")
self.assertContains(response, "Active requests")
def test_save_model_sets_restricted_status_on_user(self):
with less_console_noise():
# make sure there is no user with this email

View file

@ -98,7 +98,7 @@ class TestPopulateOrganizationType(MockEppLib):
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv", debug=True)
call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv")
def assert_expected_org_values_on_request_and_info(
self,
@ -107,9 +107,23 @@ class TestPopulateOrganizationType(MockEppLib):
expected_values: dict,
):
"""
This is a a helper function that ensures that:
This is a helper function that tests the following conditions:
1. DomainRequest and DomainInformation (on given objects) are equivalent
2. That generic_org_type, is_election_board, and organization_type are equal to passed in values
Args:
domain_request (DomainRequest): The DomainRequest object to test
domain_info (DomainInformation): The DomainInformation object to test
expected_values (dict): Container for what we expect is_electionboard, generic_org_type,
and organization_type to be on DomainRequest and DomainInformation.
Example:
expected_values = {
"is_election_board": False,
"generic_org_type": DomainRequest.OrganizationChoices.CITY,
"organization_type": DomainRequest.OrgChoicesElectionOffice.CITY,
}
"""
# Test domain request
@ -124,8 +138,23 @@ class TestPopulateOrganizationType(MockEppLib):
self.assertEqual(domain_info.is_election_board, expected_values["is_election_board"])
self.assertEqual(domain_info.organization_type, expected_values["organization_type"])
def do_nothing(self):
"""Does nothing for mocking purposes"""
pass
def test_request_and_info_city_not_in_csv(self):
"""Tests what happens to a city domain that is not defined in the CSV"""
"""
Tests what happens to a city domain that is not defined in the CSV.
Scenario: A domain request (of type city) is made that is not defined in the CSV file.
When a domain request is made for a city that is not listed in the CSV,
Then the `is_election_board` value should remain False,
and the `generic_org_type` and `organization_type` should both be `city`.
Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
The `organization_type` field should now be `city`.
"""
city_request = self.domain_request_2
city_info = self.domain_request_2
@ -149,7 +178,17 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
def test_request_and_info_federal(self):
"""Tests what happens to a federal domain after the script is run (should be unchanged)"""
"""
Tests what happens to a federal domain after the script is run (should be unchanged).
Scenario: A domain request (of type federal) is processed after running the populate_organization_type script.
When a federal domain request is made,
Then the `is_election_board` value should remain None,
and the `generic_org_type` and `organization_type` fields should both be `federal`.
Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
The `organization_type` field should now be `federal`.
"""
federal_request = self.domain_request_1
federal_info = self.domain_info_1
@ -172,10 +211,6 @@ class TestPopulateOrganizationType(MockEppLib):
# All values should be the same
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
def do_nothing(self):
"""Does nothing for mocking purposes"""
pass
def test_request_and_info_tribal_add_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION
@ -216,11 +251,14 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
def test_request_and_info_tribal_remove_election_office(self):
def test_request_and_info_tribal_doesnt_remove_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL
when it used to be TRIBAL - ELECTION
for the domain request and the domain info
Tests if a tribal domain in the election csv changes organization_type to TRIBAL_ELECTION
when the is_election_board is True, and generic_org_type is Tribal when it is not
present in the CSV.
To avoid overwriting data, the script should not set any domain specified as
an election_office (that doesn't exist in the CSV) to false.
"""
# Set org type fields to none to mimic an environment without this data
@ -252,10 +290,10 @@ class TestPopulateOrganizationType(MockEppLib):
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
# Because we don't define this in the "csv", we expect that is election board will switch to False,
# and organization_type will now be tribal
expected_values["is_election_board"] = False
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL
# If we don't define this in the "csv", but the value was already true,
# we expect that is election board will stay True, and the org type will be tribal,
# and organization_type will now be tribal_election
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
tribal_election_request.refresh_from_db()
tribal_election_info.refresh_from_db()
self.assert_expected_org_values_on_request_and_info(

View file

@ -37,7 +37,6 @@ class TestGroups(TestCase):
"add_domaininvitation",
"view_domaininvitation",
"change_domainrequest",
"change_draftdomain",
"add_federalagency",
"change_federalagency",
"delete_federalagency",
@ -48,7 +47,6 @@ class TestGroups(TestCase):
"add_verifiedbystaff",
"change_verifiedbystaff",
"delete_verifiedbystaff",
"change_website",
]
# Get the codenames of actual permissions associated with the group

View file

@ -1004,6 +1004,8 @@ class TestUser(TestCase):
Domain.objects.all().delete()
DomainInvitation.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
DraftDomain.objects.all().delete()
TransitionDomain.objects.all().delete()
User.objects.all().delete()
UserDomainRole.objects.all().delete()
@ -1060,6 +1062,91 @@ class TestUser(TestCase):
# Domain Invitation, then save routine should be called exactly once
save_mock.assert_called_once()
def test_approved_domains_count(self):
"""Test that the correct approved domain count is returned for a user"""
# with no associated approved domains, expect this to return 0
self.assertEquals(self.user.get_approved_domains_count(), 0)
# with one approved domain, expect this to return 1
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 1)
# with one approved domain, expect this to return 1 (domain2 is deleted, so not considered approved)
domain2, _ = Domain.objects.get_or_create(name="igorville2.gov", state=Domain.State.DELETED)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain2, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 1)
# with two approved domains, expect this to return 2
domain3, _ = Domain.objects.get_or_create(name="igorville3.gov", state=Domain.State.DNS_NEEDED)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain3, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 2)
# with three approved domains, expect this to return 3
domain4, _ = Domain.objects.get_or_create(name="igorville4.gov", state=Domain.State.ON_HOLD)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain4, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 3)
# with four approved domains, expect this to return 4
domain5, _ = Domain.objects.get_or_create(name="igorville5.gov", state=Domain.State.READY)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain5, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 4)
def test_active_requests_count(self):
"""Test that the correct active domain requests count is returned for a user"""
# with no associated active requests, expect this to return 0
self.assertEquals(self.user.get_active_requests_count(), 0)
# with one active request, expect this to return 1
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.SUBMITTED
)
self.assertEquals(self.user.get_active_requests_count(), 1)
# with two active requests, expect this to return 2
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville2.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.IN_REVIEW
)
self.assertEquals(self.user.get_active_requests_count(), 2)
# with three active requests, expect this to return 3
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville3.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.ACTION_NEEDED
)
self.assertEquals(self.user.get_active_requests_count(), 3)
# with three active requests, expect this to return 3 (STARTED is not considered active)
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville4.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.STARTED
)
self.assertEquals(self.user.get_active_requests_count(), 3)
def test_rejected_requests_count(self):
"""Test that the correct rejected domain requests count is returned for a user"""
# with no associated rejected requests, expect this to return 0
self.assertEquals(self.user.get_rejected_requests_count(), 0)
# with one rejected request, expect this to return 1
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.REJECTED
)
self.assertEquals(self.user.get_rejected_requests_count(), 1)
def test_ineligible_requests_count(self):
"""Test that the correct ineligible domain requests count is returned for a user"""
# with no associated ineligible requests, expect this to return 0
self.assertEquals(self.user.get_ineligible_requests_count(), 0)
# with one ineligible request, expect this to return 1
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.INELIGIBLE
)
self.assertEquals(self.user.get_ineligible_requests_count(), 1)
def test_has_contact_info(self):
"""Test that has_contact_info properly returns"""
# test with a user with contact info defined
self.assertTrue(self.user.has_contact_info())
# test with a user without contact info defined
self.user.contact.title = None
self.user.contact.email = None
self.user.contact.phone = None
self.assertFalse(self.user.has_contact_info())
class TestContact(TestCase):
def setUp(self):
@ -1162,6 +1249,16 @@ class TestContact(TestCase):
self.assertFalse(self.contact_as_ao.has_more_than_one_join("authorizing_official"))
self.assertTrue(self.contact_as_ao.has_more_than_one_join("submitted_domain_requests"))
def test_has_contact_info(self):
"""Test that has_contact_info properly returns"""
# test with a contact with contact info defined
self.assertTrue(self.contact.has_contact_info())
# test with a contact without contact info defined
self.contact.title = None
self.contact.email = None
self.contact.phone = None
self.assertFalse(self.contact.has_contact_info())
class TestDomainRequestCustomSave(TestCase):
"""Tests custom save behaviour on the DomainRequest object"""

View file

@ -5,7 +5,7 @@ from django.conf import settings
from django.urls import reverse
from django.contrib.auth import get_user_model
from .common import MockSESClient, create_user # type: ignore
from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -71,11 +71,14 @@ class TestWithDomainPermissions(TestWithUser):
# that inherit this setUp
self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov")
self.domain_with_four_nameservers, _ = Domain.objects.get_or_create(name="fournameserversDomain.gov")
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_four_nameservers)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold)
@ -98,6 +101,11 @@ class TestWithDomainPermissions(TestWithUser):
domain=self.domain_dnssec_none,
role=UserDomainRole.Roles.MANAGER,
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_with_four_nameservers,
role=UserDomainRole.Roles.MANAGER,
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_with_ip,
@ -727,7 +735,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertContains(home_page, self.domain.name)
class TestDomainNameservers(TestDomainOverview):
class TestDomainNameservers(TestDomainOverview, MockEppLib):
def test_domain_nameservers(self):
"""Can load domain's nameservers page."""
page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
@ -974,6 +982,117 @@ class TestDomainNameservers(TestDomainOverview):
page = result.follow()
self.assertContains(page, "The name servers for this domain have been updated")
def test_domain_nameservers_can_blank_out_first_or_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first or
second entries are blanked out.
Uses self.app WebTest because we need to interact with forms.
"""
nameserver1 = ""
nameserver2 = "ns2.igorville.gov"
nameserver3 = "ns3.igorville.gov"
valid_ip = ""
valid_ip_2 = "128.0.0.2"
valid_ip_3 = "128.0.0.3"
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
nameserver1 = "ns1.igorville.gov"
nameserver2 = ""
nameserver3 = "ns3.igorville.gov"
valid_ip = "128.0.0.1"
valid_ip_2 = ""
valid_ip_3 = "128.0.0.3"
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
def test_domain_nameservers_can_blank_out_first_and_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first and
second entries are blanked out.
Uses self.app WebTest because we need to interact with forms.
"""
# We need to start with a domain with 4 nameservers otherwise the formset in the test environment
# will only have 3 forms
nameserver1 = ""
nameserver2 = ""
nameserver3 = "ns3.igorville.gov"
nameserver4 = "ns4.igorville.gov"
valid_ip = ""
valid_ip_2 = ""
valid_ip_3 = ""
valid_ip_4 = ""
nameservers_page = self.app.get(
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Minimal check to ensure the form is loaded correctly
self.assertEqual(nameservers_page.form["form-0-server"].value, "ns1.my-nameserver-1.com")
self.assertEqual(nameservers_page.form["form-3-server"].value, "ns1.explosive-chicken-nuggets.com")
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
nameservers_page.form["form-3-server"] = nameserver4
nameservers_page.form["form-3-ip"] = valid_ip_4
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
def test_domain_nameservers_form_invalid(self):
"""Nameserver form does not submit with invalid data.