mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-20 17:56:11 +02:00
Merge branch 'main' into za/1816-domain-metadata-includes-organization-type
This commit is contained in:
commit
9aa7235105
30 changed files with 989 additions and 152 deletions
62
.github/ISSUE_TEMPLATE/designer-onboarding.md
vendored
Normal file
62
.github/ISSUE_TEMPLATE/designer-onboarding.md
vendored
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -588,14 +588,23 @@ Example: `cf ssh getgov-za`
|
|||
| 1 | **debug** | Increases logging detail. Defaults to False. |
|
||||
|
||||
|
||||
## Populate First Ready
|
||||
## 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.
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
37
src/registrar/migrations/0084_create_groups_v11.py
Normal file
37
src/registrar/migrations/0084_create_groups_v11.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 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,
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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. We’ll 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. We’ll 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?
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue