mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-21 11:59:24 +02:00
merged main
This commit is contained in:
commit
5761a8a16e
27 changed files with 1247 additions and 72 deletions
61
.github/ISSUE_TEMPLATE/designer-onboarding.md
vendored
Normal file
61
.github/ISSUE_TEMPLATE/designer-onboarding.md
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
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
|
||||
- [ ] Once onboardee has been granted a Figma license, add them as an editor to the Figma team workspace, Otherwise, you can add them as a viewer in the meantime.
|
||||
- [ ] 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.
|
||||
|
|
|
@ -19,6 +19,18 @@ role or set of permissions that they have. We use a `UserDomainRole`
|
|||
`User.domains` many-to-many relationship that works through the
|
||||
`UserDomainRole` link table.
|
||||
|
||||
## Migrating changes to Analyst Permissions model
|
||||
Analysts are allowed a certain set of read/write registrar permissions.
|
||||
Setting user permissions requires a migration to change the UserGroup
|
||||
and Permission models, which requires us to manually make a migration
|
||||
file for user permission changes.
|
||||
To update analyst permissions do the following:
|
||||
1. Make desired changes to analyst group permissions in user_group.py.
|
||||
2. Follow the steps in the migration file0037_create_groups_v01.py to
|
||||
create a duplicate migration for the updated user group permissions.
|
||||
3. To migrate locally, run docker-compose up. To migrate on a sandbox,
|
||||
push the new migration onto your sandbox before migrating.
|
||||
|
||||
## Permission decorator
|
||||
|
||||
The Django objects that need to be permission controlled are various views.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -779,6 +779,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."""
|
||||
|
@ -901,6 +941,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"fields": [
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
]
|
||||
},
|
||||
),
|
||||
|
@ -943,7 +984,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
]
|
||||
|
||||
# Readonly fields for analysts and superusers
|
||||
readonly_fields = ("other_contacts",)
|
||||
readonly_fields = ("other_contacts", "generic_org_type", "is_election_board")
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
|
@ -1139,6 +1180,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
"fields": [
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
]
|
||||
},
|
||||
),
|
||||
|
@ -1181,7 +1223,13 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
]
|
||||
|
||||
# Readonly fields for analysts and superusers
|
||||
readonly_fields = ("other_contacts", "current_websites", "alternative_domains")
|
||||
readonly_fields = (
|
||||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
)
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
|
@ -1458,7 +1506,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)
|
||||
|
||||
|
@ -1619,11 +1670,7 @@ 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)
|
||||
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
|
||||
|
@ -1888,6 +1935,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."""
|
||||
|
|
|
@ -98,6 +98,8 @@ class DomainRequestFixture:
|
|||
def _set_non_foreign_key_fields(cls, da: DomainRequest, app: dict):
|
||||
"""Helper method used by `load`."""
|
||||
da.status = app["status"] if "status" in app else "started"
|
||||
|
||||
# TODO for a future ticket: Allow for more than just "federal" here
|
||||
da.generic_org_type = app["generic_org_type"] if "generic_org_type" in app else "federal"
|
||||
da.federal_agency = (
|
||||
app["federal_agency"]
|
||||
|
@ -235,9 +237,6 @@ class DomainFixture(DomainRequestFixture):
|
|||
).last()
|
||||
logger.debug(f"Approving {domain_request} for {user}")
|
||||
|
||||
# We don't want fixtures sending out real emails to
|
||||
# fake email addresses, so we just skip that and log it instead
|
||||
|
||||
# All approvals require an investigator, so if there is none,
|
||||
# assign one.
|
||||
if domain_request.investigator is None:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import logging
|
||||
from django import forms
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
||||
from django.forms import formset_factory
|
||||
from registrar.models import DomainRequest
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
@ -31,7 +31,17 @@ logger = logging.getLogger(__name__)
|
|||
class DomainAddUserForm(forms.Form):
|
||||
"""Form for adding a user to a domain."""
|
||||
|
||||
email = forms.EmailField(label="Email")
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""clean form data by lowercasing email"""
|
||||
|
@ -171,6 +181,8 @@ NameserverFormset = formset_factory(
|
|||
class ContactForm(forms.ModelForm):
|
||||
"""Form for updating contacts."""
|
||||
|
||||
email = forms.EmailField(max_length=None)
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||
|
@ -194,6 +206,10 @@ class ContactForm(forms.ModelForm):
|
|||
# which interferes with out input_with_errors template tag
|
||||
self.fields["phone"].widget.attrs.pop("maxlength", None)
|
||||
|
||||
# Define a custom validator for the email field with a custom error message
|
||||
email_max_length_validator = MaxLengthValidator(320, message="Response must be less than 320 characters.")
|
||||
self.fields["email"].validators.append(email_max_length_validator)
|
||||
|
||||
for field_name in self.required:
|
||||
self.fields[field_name].required = True
|
||||
|
||||
|
@ -291,10 +307,17 @@ class DomainSecurityEmailForm(forms.Form):
|
|||
|
||||
security_email = forms.EmailField(
|
||||
label="Security email (optional)",
|
||||
max_length=None,
|
||||
required=False,
|
||||
error_messages={
|
||||
"invalid": str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)),
|
||||
},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -369,7 +369,14 @@ class AuthorizingOfficialForm(RegistrarForm):
|
|||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={"invalid": ("Enter an email address in the required format, like name@example.com.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
@ -566,7 +573,14 @@ class YourContactForm(RegistrarForm):
|
|||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
label="Phone",
|
||||
|
@ -621,10 +635,17 @@ class OtherContactsForm(RegistrarForm):
|
|||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={
|
||||
"required": ("Enter an email address in the required format, like name@example.com."),
|
||||
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
||||
},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
label="Phone",
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# Generated by Django 4.2.10 on 2024-04-01 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0081_create_groups_v10"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domaininformation",
|
||||
name="organization_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
("state_or_territory_election", "State or territory - Election"),
|
||||
("tribal_election", "Tribal - Election"),
|
||||
("county_election", "County - Election"),
|
||||
("city_election", "City - Election"),
|
||||
("special_district_election", "Special district - Election"),
|
||||
],
|
||||
help_text="Type of organization - Election office",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="organization_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
("state_or_territory_election", "State or territory - Election"),
|
||||
("tribal_election", "Tribal - Election"),
|
||||
("county_election", "County - Election"),
|
||||
("city_election", "City - Election"),
|
||||
("special_district_election", "Special district - Election"),
|
||||
],
|
||||
help_text="Type of organization - Election office",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="generic_org_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
],
|
||||
help_text="Type of organization",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.2.10 on 2024-04-09 16:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0082_domaininformation_organization_type_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="email",
|
||||
field=models.EmailField(blank=True, db_index=True, max_length=320, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="publiccontact",
|
||||
name="email",
|
||||
field=models.EmailField(help_text="Contact's email address", max_length=320),
|
||||
),
|
||||
]
|
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,
|
||||
),
|
||||
]
|
|
@ -40,6 +40,7 @@ class Contact(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
max_length=320,
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
null=True,
|
||||
|
|
|
@ -198,7 +198,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
is called in the validate function on the request/domain page
|
||||
|
||||
throws- RegistryError or InvalidDomainError"""
|
||||
|
||||
if not cls.string_could_be_domain(domain):
|
||||
logger.warning("Not a valid domain: %s" % str(domain))
|
||||
# throw invalid domain error so that it can be caught in
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
from django.db import transaction
|
||||
|
||||
from registrar.models.utility.domain_helper import DomainHelper
|
||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||
from .domain_request import DomainRequest
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
@ -54,7 +55,23 @@ class DomainInformation(TimeStampedModel):
|
|||
choices=OrganizationChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of Organization",
|
||||
help_text="Type of organization",
|
||||
)
|
||||
|
||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||
organization_type = models.CharField(
|
||||
max_length=255,
|
||||
choices=DomainRequest.OrgChoicesElectionOffice.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of organization - Election office",
|
||||
)
|
||||
|
||||
federally_recognized_tribe = models.BooleanField(
|
||||
|
@ -219,6 +236,34 @@ class DomainInformation(TimeStampedModel):
|
|||
except Exception:
|
||||
return ""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
|
||||
# Define mappings between generic org and election org.
|
||||
# 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 example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
|
||||
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()
|
||||
|
||||
# For any given "_election" variant, return the base org type.
|
||||
# For example: STATE_OR_TERRITORY_ELECTION => STATE_OR_TERRITORY
|
||||
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"
|
||||
org_type_helper = CreateOrUpdateOrganizationTypeHelper(
|
||||
sender=self.__class__,
|
||||
instance=self,
|
||||
generic_org_to_org_map=generic_org_map,
|
||||
election_org_to_generic_org_map=election_org_map,
|
||||
)
|
||||
|
||||
# Actually updates the organization_type field
|
||||
org_type_helper.create_or_update_organization_type()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def create_from_da(cls, domain_request: DomainRequest, domain=None):
|
||||
"""Takes in a DomainRequest and converts it into DomainInformation"""
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.db import models
|
|||
from django_fsm import FSMField, transition # type: ignore
|
||||
from django.utils import timezone
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
@ -100,8 +101,8 @@ class DomainRequest(TimeStampedModel):
|
|||
class OrganizationChoices(models.TextChoices):
|
||||
"""
|
||||
Primary organization choices:
|
||||
For use in django admin
|
||||
Keys need to match OrganizationChoicesVerbose
|
||||
For use in the domain request experience
|
||||
Keys need to match OrgChoicesElectionOffice and OrganizationChoicesVerbose
|
||||
"""
|
||||
|
||||
FEDERAL = "federal", "Federal"
|
||||
|
@ -113,9 +114,77 @@ class DomainRequest(TimeStampedModel):
|
|||
SPECIAL_DISTRICT = "special_district", "Special district"
|
||||
SCHOOL_DISTRICT = "school_district", "School district"
|
||||
|
||||
class OrgChoicesElectionOffice(models.TextChoices):
|
||||
"""
|
||||
Primary organization choices for Django admin:
|
||||
Keys need to match OrganizationChoices and OrganizationChoicesVerbose.
|
||||
|
||||
The enums here come in two variants:
|
||||
Regular (matches the choices from OrganizationChoices)
|
||||
Election (Appends " - Election" to the string)
|
||||
|
||||
When adding the election variant, you must append "_election" to the end of the string.
|
||||
"""
|
||||
|
||||
# We can't inherit OrganizationChoices due to models.TextChoices being an enum.
|
||||
# We can redefine these values instead.
|
||||
FEDERAL = "federal", "Federal"
|
||||
INTERSTATE = "interstate", "Interstate"
|
||||
STATE_OR_TERRITORY = "state_or_territory", "State or territory"
|
||||
TRIBAL = "tribal", "Tribal"
|
||||
COUNTY = "county", "County"
|
||||
CITY = "city", "City"
|
||||
SPECIAL_DISTRICT = "special_district", "Special district"
|
||||
SCHOOL_DISTRICT = "school_district", "School district"
|
||||
|
||||
# Election variants
|
||||
STATE_OR_TERRITORY_ELECTION = "state_or_territory_election", "State or territory - Election"
|
||||
TRIBAL_ELECTION = "tribal_election", "Tribal - Election"
|
||||
COUNTY_ELECTION = "county_election", "County - Election"
|
||||
CITY_ELECTION = "city_election", "City - Election"
|
||||
SPECIAL_DISTRICT_ELECTION = "special_district_election", "Special district - Election"
|
||||
|
||||
@classmethod
|
||||
def get_org_election_to_org_generic(cls):
|
||||
"""
|
||||
Creates and returns a dictionary mapping from election-specific organization
|
||||
choice enums to their corresponding general organization choice enums.
|
||||
|
||||
If no such mapping exists, it is simple excluded from the map.
|
||||
"""
|
||||
# This can be mapped automatically but its harder to read.
|
||||
# For clarity reasons, we manually define this.
|
||||
org_election_map = {
|
||||
cls.STATE_OR_TERRITORY_ELECTION: cls.STATE_OR_TERRITORY,
|
||||
cls.TRIBAL_ELECTION: cls.TRIBAL,
|
||||
cls.COUNTY_ELECTION: cls.COUNTY,
|
||||
cls.CITY_ELECTION: cls.CITY,
|
||||
cls.SPECIAL_DISTRICT_ELECTION: cls.SPECIAL_DISTRICT,
|
||||
}
|
||||
return org_election_map
|
||||
|
||||
@classmethod
|
||||
def get_org_generic_to_org_election(cls):
|
||||
"""
|
||||
Creates and returns a dictionary mapping from general organization
|
||||
choice enums to their corresponding election-specific organization enums.
|
||||
|
||||
If no such mapping exists, it is simple excluded from the map.
|
||||
"""
|
||||
# This can be mapped automatically but its harder to read.
|
||||
# For clarity reasons, we manually define this.
|
||||
org_election_map = {
|
||||
cls.STATE_OR_TERRITORY: cls.STATE_OR_TERRITORY_ELECTION,
|
||||
cls.TRIBAL: cls.TRIBAL_ELECTION,
|
||||
cls.COUNTY: cls.COUNTY_ELECTION,
|
||||
cls.CITY: cls.CITY_ELECTION,
|
||||
cls.SPECIAL_DISTRICT: cls.SPECIAL_DISTRICT_ELECTION,
|
||||
}
|
||||
return org_election_map
|
||||
|
||||
class OrganizationChoicesVerbose(models.TextChoices):
|
||||
"""
|
||||
Secondary organization choices
|
||||
Tertiary organization choices
|
||||
For use in the domain request form and on the templates
|
||||
Keys need to match OrganizationChoices
|
||||
"""
|
||||
|
@ -406,6 +475,21 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Type of organization",
|
||||
)
|
||||
|
||||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||
organization_type = models.CharField(
|
||||
max_length=255,
|
||||
choices=OrgChoicesElectionOffice.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of organization - Election office",
|
||||
)
|
||||
|
||||
federally_recognized_tribe = models.BooleanField(
|
||||
null=True,
|
||||
help_text="Is the tribe federally recognized",
|
||||
|
@ -437,18 +521,13 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Federal government branch",
|
||||
)
|
||||
|
||||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
organization_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Organization name",
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
address_line1 = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -525,6 +604,7 @@ class DomainRequest(TimeStampedModel):
|
|||
related_name="domain_request",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
alternative_domains = models.ManyToManyField(
|
||||
"registrar.Website",
|
||||
blank=True,
|
||||
|
@ -586,6 +666,34 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Notes about this request",
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
|
||||
# Define mappings between generic org and election org.
|
||||
# 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 example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
|
||||
generic_org_map = self.OrgChoicesElectionOffice.get_org_generic_to_org_election()
|
||||
|
||||
# For any given "_election" variant, return the base org type.
|
||||
# For example: STATE_OR_TERRITORY_ELECTION => STATE_OR_TERRITORY
|
||||
election_org_map = self.OrgChoicesElectionOffice.get_org_election_to_org_generic()
|
||||
|
||||
# Manages the "organization_type" variable and keeps in sync with
|
||||
# "is_election_office" and "generic_organization_type"
|
||||
org_type_helper = CreateOrUpdateOrganizationTypeHelper(
|
||||
sender=self.__class__,
|
||||
instance=self,
|
||||
generic_org_to_org_map=generic_org_map,
|
||||
election_org_to_generic_org_map=election_org_map,
|
||||
)
|
||||
|
||||
# Actually updates the organization_type field
|
||||
org_type_helper.create_or_update_organization_type()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.requested_domain and self.requested_domain.name:
|
||||
|
|
|
@ -68,7 +68,7 @@ class PublicContact(TimeStampedModel):
|
|||
sp = models.CharField(null=False, help_text="Contact's state or province")
|
||||
pc = models.CharField(null=False, help_text="Contact's postal code")
|
||||
cc = models.CharField(null=False, help_text="Contact's country code")
|
||||
email = models.EmailField(null=False, help_text="Contact's email address")
|
||||
email = models.EmailField(null=False, help_text="Contact's email address", max_length=320)
|
||||
voice = models.CharField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format")
|
||||
fax = models.CharField(
|
||||
null=True,
|
||||
|
|
|
@ -5,6 +5,11 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class UserGroup(Group):
|
||||
"""
|
||||
UserGroup sets read and write permissions for superusers (who have full access)
|
||||
and analysts. For more details, see the dev docs for user-permissions.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = "User group"
|
||||
verbose_name_plural = "User groups"
|
||||
|
@ -36,11 +41,6 @@ class UserGroup(Group):
|
|||
"model": "domain",
|
||||
"permissions": ["view_domain"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "draftdomain",
|
||||
"permissions": ["change_draftdomain"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "user",
|
||||
|
@ -51,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",
|
||||
|
|
|
@ -35,3 +35,219 @@ class Timer:
|
|||
self.end = time.time()
|
||||
self.duration = self.end - self.start
|
||||
logger.info(f"Execution time: {self.duration} seconds")
|
||||
|
||||
|
||||
class CreateOrUpdateOrganizationTypeHelper:
|
||||
"""
|
||||
A helper that manages the "organization_type" field in DomainRequest and DomainInformation
|
||||
"""
|
||||
|
||||
def __init__(self, sender, instance, generic_org_to_org_map, election_org_to_generic_org_map):
|
||||
# The "model type"
|
||||
self.sender = sender
|
||||
self.instance = instance
|
||||
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):
|
||||
"""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
|
||||
values.
|
||||
|
||||
If the instance is marked as an election board and the generic_org_type is not
|
||||
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.
|
||||
"""
|
||||
|
||||
# A new record is added with organization_type not defined.
|
||||
# This happens from the regular domain request flow.
|
||||
is_new_instance = self.instance.id is None
|
||||
if is_new_instance:
|
||||
self._handle_new_instance()
|
||||
else:
|
||||
self._handle_existing_instance()
|
||||
|
||||
return self.instance
|
||||
|
||||
def _handle_new_instance(self):
|
||||
# == Check for invalid conditions before proceeding == #
|
||||
should_proceed = self._validate_new_instance()
|
||||
if not should_proceed:
|
||||
return None
|
||||
# == Program flow will halt here if there is no reason to update == #
|
||||
|
||||
# == Update the linked values == #
|
||||
organization_type_needs_update = self.instance.organization_type is None
|
||||
generic_org_type_needs_update = self.instance.generic_org_type is None
|
||||
|
||||
# If a field is none, it indicates (per prior checks) that the
|
||||
# related field (generic org type <-> org type) has data and we should update according to that.
|
||||
if organization_type_needs_update:
|
||||
self._update_org_type_from_generic_org_and_election()
|
||||
elif generic_org_type_needs_update:
|
||||
self._update_generic_org_and_election_from_org_type()
|
||||
|
||||
# Update the field
|
||||
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
||||
|
||||
def _handle_existing_instance(self):
|
||||
# == Init variables == #
|
||||
# Instance is already in the database, fetch its current state
|
||||
current_instance = self.sender.objects.get(id=self.instance.id)
|
||||
|
||||
# Check the new and old values
|
||||
generic_org_type_changed = self.instance.generic_org_type != current_instance.generic_org_type
|
||||
is_election_board_changed = self.instance.is_election_board != current_instance.is_election_board
|
||||
organization_type_changed = self.instance.organization_type != current_instance.organization_type
|
||||
|
||||
# == Check for invalid conditions before proceeding == #
|
||||
if organization_type_changed and (generic_org_type_changed or is_election_board_changed):
|
||||
# Since organization type is linked with generic_org_type and election board,
|
||||
# we have to update one or the other, not both.
|
||||
# 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 == #
|
||||
|
||||
# == 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)
|
||||
|
||||
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
|
||||
"""
|
||||
Validates the conditions for updating organization and generic organization types.
|
||||
|
||||
Raises:
|
||||
ValueError: If both organization_type_needs_update and generic_org_type_needs_update are True,
|
||||
indicating an attempt to update both fields simultaneously, which is not allowed.
|
||||
"""
|
||||
# We shouldn't update both of these at the same time.
|
||||
# It is more useful to have these as seperate variables, but it does impose
|
||||
# this restraint.
|
||||
if organization_type_needs_update and generic_org_type_needs_update:
|
||||
raise ValueError("Cannot update both org type and generic org type at the same time.")
|
||||
|
||||
if organization_type_needs_update:
|
||||
self._update_org_type_from_generic_org_and_election()
|
||||
elif generic_org_type_needs_update:
|
||||
self._update_generic_org_and_election_from_org_type()
|
||||
|
||||
def _update_org_type_from_generic_org_and_election(self):
|
||||
"""Given a field values for generic_org_type and is_election_board, update the
|
||||
organization_type field."""
|
||||
|
||||
# We convert to a string because the enum types are different.
|
||||
generic_org_type = str(self.instance.generic_org_type)
|
||||
if generic_org_type not in self.generic_org_to_org_map:
|
||||
# Election board should always be reset to None if the record
|
||||
# can't have one. For example, federal.
|
||||
if self.instance.is_election_board is not None:
|
||||
# This maintains data consistency.
|
||||
# There is no avenue for this to occur in the UI,
|
||||
# as such - this can only occur if the object is initialized in this way.
|
||||
# Or if there are pre-existing data.
|
||||
logger.warning(
|
||||
"create_or_update_organization_type() -> is_election_board "
|
||||
f"cannot exist for {generic_org_type}. Setting to None."
|
||||
)
|
||||
self.instance.is_election_board = None
|
||||
self.instance.organization_type = generic_org_type
|
||||
else:
|
||||
# This can only happen with manual data tinkering, which causes these to be out of sync.
|
||||
if self.instance.is_election_board is None:
|
||||
logger.warning(
|
||||
"create_or_update_organization_type() -> is_election_board is out of sync. Updating value."
|
||||
)
|
||||
self.instance.is_election_board = False
|
||||
|
||||
if self.instance.is_election_board:
|
||||
self.instance.organization_type = self.generic_org_to_org_map[generic_org_type]
|
||||
else:
|
||||
self.instance.organization_type = generic_org_type
|
||||
|
||||
def _update_generic_org_and_election_from_org_type(self):
|
||||
"""Given the field value for organization_type, update the
|
||||
generic_org_type and is_election_board field."""
|
||||
|
||||
# We convert to a string because the enum types are different
|
||||
# between OrgChoicesElectionOffice and OrganizationChoices.
|
||||
# But their names are the same (for the most part).
|
||||
current_org_type = str(self.instance.organization_type)
|
||||
election_org_map = self.election_org_to_generic_org_map
|
||||
generic_org_map = self.generic_org_to_org_map
|
||||
|
||||
# This essentially means: "_election" in current_org_type.
|
||||
if current_org_type in election_org_map:
|
||||
new_org = election_org_map[current_org_type]
|
||||
self.instance.generic_org_type = new_org
|
||||
self.instance.is_election_board = True
|
||||
elif self.instance.organization_type is not None:
|
||||
self.instance.generic_org_type = current_org_type
|
||||
|
||||
# This basically checks if the given org type
|
||||
# can even have an election board in the first place.
|
||||
# For instance, federal cannot so is_election_board = None
|
||||
if current_org_type in generic_org_map:
|
||||
self.instance.is_election_board = False
|
||||
else:
|
||||
# This maintains data consistency.
|
||||
# There is no avenue for this to occur in the UI,
|
||||
# as such - this can only occur if the object is initialized in this way.
|
||||
# Or if there are pre-existing data.
|
||||
logger.warning(
|
||||
"create_or_update_organization_type() -> is_election_board "
|
||||
f"cannot exist for {current_org_type}. Setting to None."
|
||||
)
|
||||
self.instance.is_election_board = None
|
||||
else:
|
||||
# if self.instance.organization_type is set to None, then this means
|
||||
# we should clear the related fields.
|
||||
# This will not occur if it just is None (i.e. default), only if it is set to be so.
|
||||
self.instance.is_election_board = None
|
||||
self.instance.generic_org_type = None
|
||||
|
||||
def _validate_new_instance(self):
|
||||
"""
|
||||
Validates whether a new instance of DomainRequest or DomainInformation can proceed with the update
|
||||
based on the consistency between organization_type, generic_org_type, and is_election_board.
|
||||
|
||||
Returns a boolean determining if execution should proceed or not.
|
||||
"""
|
||||
|
||||
# We conditionally accept both of these values to exist simultaneously, as long as
|
||||
# those values do not intefere with eachother.
|
||||
# Because this condition can only be triggered through a dev (no user flow),
|
||||
# we throw an error if an invalid state is found here.
|
||||
if self.instance.organization_type and self.instance.generic_org_type:
|
||||
generic_org_type = str(self.instance.generic_org_type)
|
||||
organization_type = str(self.instance.organization_type)
|
||||
|
||||
# Strip "_election" if it exists
|
||||
mapped_org_type = self.election_org_to_generic_org_map.get(organization_type)
|
||||
|
||||
# Do tests on the org update for election board changes.
|
||||
is_election_type = "_election" in organization_type
|
||||
can_have_election_board = organization_type in self.generic_org_to_org_map
|
||||
|
||||
election_board_mismatch = (is_election_type != self.instance.is_election_board) and can_have_election_board
|
||||
org_type_mismatch = mapped_org_type is not None and (generic_org_type != mapped_org_type)
|
||||
if election_board_mismatch or org_type_mismatch:
|
||||
message = (
|
||||
"Cannot add organization_type and generic_org_type simultaneously "
|
||||
"when generic_org_type, is_election_board, and organization_type values do not match."
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
return True
|
||||
elif not self.instance.organization_type and not self.instance.generic_org_type:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -585,7 +585,7 @@ class MockDb(TestCase):
|
|||
generic_org_type="federal",
|
||||
federal_agency="World War I Centennial Commission",
|
||||
federal_type="executive",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain_2, generic_org_type="interstate", is_election_board=True
|
||||
|
@ -595,14 +595,14 @@ class MockDb(TestCase):
|
|||
domain=self.domain_3,
|
||||
generic_org_type="federal",
|
||||
federal_agency="Armed Forces Retirement Home",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
domain=self.domain_4,
|
||||
generic_org_type="federal",
|
||||
federal_agency="Armed Forces Retirement Home",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
|
@ -652,7 +652,7 @@ class MockDb(TestCase):
|
|||
generic_org_type="federal",
|
||||
federal_agency="World War I Centennial Commission",
|
||||
federal_type="executive",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_12, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
|
@ -801,6 +801,9 @@ def completed_domain_request(
|
|||
submitter=False,
|
||||
name="city.gov",
|
||||
investigator=None,
|
||||
generic_org_type="federal",
|
||||
is_election_board=False,
|
||||
organization_type=None,
|
||||
):
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
|
@ -838,7 +841,8 @@ def completed_domain_request(
|
|||
is_staff=True,
|
||||
)
|
||||
domain_request_kwargs = dict(
|
||||
generic_org_type="federal",
|
||||
generic_org_type=generic_org_type,
|
||||
is_election_board=is_election_board,
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
is_policy_acknowledged=True,
|
||||
|
@ -859,6 +863,9 @@ def completed_domain_request(
|
|||
if has_anything_else:
|
||||
domain_request_kwargs["anything_else"] = "There is more"
|
||||
|
||||
if organization_type:
|
||||
domain_request_kwargs["organization_type"] = organization_type
|
||||
|
||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||
|
||||
if has_other_contacts:
|
||||
|
|
|
@ -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():
|
||||
|
@ -1656,6 +1779,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
|
@ -1664,12 +1789,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"creator",
|
||||
"investigator",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"tribe_name",
|
||||
"federal_agency",
|
||||
"federal_type",
|
||||
"is_election_board",
|
||||
"organization_name",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
|
@ -1704,6 +1830,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
|
@ -1729,6 +1857,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
@ -2327,6 +2457,8 @@ class TestDomainInformationAdmin(TestCase):
|
|||
|
||||
expected_fields = [
|
||||
"other_contacts",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"creator",
|
||||
"type_of_work",
|
||||
"more_organization_information",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1258,3 +1258,309 @@ class TestContact(TestCase):
|
|||
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"""
|
||||
|
||||
def tearDown(self):
|
||||
DomainRequest.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
def test_create_or_update_organization_type_new_instance(self):
|
||||
"""Test create_or_update_organization_type when creating a new instance"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
def test_create_or_update_organization_type_new_instance_federal_does_nothing(self):
|
||||
"""Test if create_or_update_organization_type does nothing when creating a new instance for federal"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL)
|
||||
self.assertEqual(domain_request.is_election_board, None)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_election_board(self):
|
||||
"""Test create_or_update_organization_type for an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_request.is_election_board = True
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, True)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
# Try reverting the election board value
|
||||
domain_request.is_election_board = False
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
# Try reverting setting an invalid value for election board (should revert to False)
|
||||
domain_request.is_election_board = None
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self):
|
||||
"""Test create_or_update_organization_type when modifying generic_org_type on an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
|
||||
domain_request.save()
|
||||
|
||||
# Election board should be None because interstate cannot have an election board.
|
||||
self.assertEqual(domain_request.is_election_board, None)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.INTERSTATE)
|
||||
|
||||
# Try changing the org Type to something that CAN have an election board.
|
||||
domain_request_tribal = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedTribal.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
domain_request_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
|
||||
)
|
||||
|
||||
# Change the org type
|
||||
domain_request_tribal.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
|
||||
domain_request_tribal.save()
|
||||
|
||||
self.assertEqual(domain_request_tribal.is_election_board, True)
|
||||
self.assertEqual(
|
||||
domain_request_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION
|
||||
)
|
||||
|
||||
def test_create_or_update_organization_type_no_update(self):
|
||||
"""Test create_or_update_organization_type when there are no values to update."""
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both non-election board
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_request.save()
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both election board
|
||||
domain_request_election = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedElection.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
organization_type=DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
domain_request_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_request_election.is_election_board, True)
|
||||
self.assertEqual(domain_request_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Modify an unrelated existing value for both, and ensure that everything is still consistent
|
||||
domain_request.city = "Fudge"
|
||||
domain_request_election.city = "Caramel"
|
||||
domain_request.save()
|
||||
domain_request_election.save()
|
||||
|
||||
self.assertEqual(domain_request.city, "Fudge")
|
||||
self.assertEqual(domain_request_election.city, "Caramel")
|
||||
|
||||
# Test for non-election
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for election
|
||||
self.assertEqual(
|
||||
domain_request_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_request_election.is_election_board, True)
|
||||
self.assertEqual(domain_request_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
|
||||
class TestDomainInformationCustomSave(TestCase):
|
||||
"""Tests custom save behaviour on the DomainInformation object"""
|
||||
|
||||
def tearDown(self):
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
def test_create_or_update_organization_type_new_instance(self):
|
||||
"""Test create_or_update_organization_type when creating a new instance"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
def test_create_or_update_organization_type_new_instance_federal_does_nothing(self):
|
||||
"""Test if create_or_update_organization_type does nothing when creating a new instance for federal"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL)
|
||||
self.assertEqual(domain_information.is_election_board, None)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_election_board(self):
|
||||
"""Test create_or_update_organization_type for an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
domain_information.is_election_board = True
|
||||
domain_information.save()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, True)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
# Try reverting the election board value
|
||||
domain_information.is_election_board = False
|
||||
domain_information.save()
|
||||
domain_information.refresh_from_db()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
# Try reverting setting an invalid value for election board (should revert to False)
|
||||
domain_information.is_election_board = None
|
||||
domain_information.save()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self):
|
||||
"""Test create_or_update_organization_type when modifying generic_org_type on an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
|
||||
domain_information.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
|
||||
domain_information.save()
|
||||
|
||||
# Election board should be None because interstate cannot have an election board.
|
||||
self.assertEqual(domain_information.is_election_board, None)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.INTERSTATE)
|
||||
|
||||
# Try changing the org Type to something that CAN have an election board.
|
||||
domain_request_tribal = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedTribal.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
domain_information_tribal = DomainInformation.create_from_da(domain_request_tribal)
|
||||
self.assertEqual(
|
||||
domain_information_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
|
||||
)
|
||||
|
||||
# Change the org type
|
||||
domain_information_tribal.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
|
||||
domain_information_tribal.save()
|
||||
|
||||
self.assertEqual(domain_information_tribal.is_election_board, True)
|
||||
self.assertEqual(
|
||||
domain_information_tribal.organization_type,
|
||||
DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION,
|
||||
)
|
||||
|
||||
def test_create_or_update_organization_type_no_update(self):
|
||||
"""Test create_or_update_organization_type when there are no values to update."""
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both non-election board
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
domain_information.save()
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both election board
|
||||
domain_request_election = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedElection.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
organization_type=DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION,
|
||||
)
|
||||
domain_information_election = DomainInformation.create_from_da(domain_request_election)
|
||||
|
||||
self.assertEqual(
|
||||
domain_information_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_information_election.is_election_board, True)
|
||||
self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Modify an unrelated existing value for both, and ensure that everything is still consistent
|
||||
domain_information.city = "Fudge"
|
||||
domain_information_election.city = "Caramel"
|
||||
domain_information.save()
|
||||
domain_information_election.save()
|
||||
|
||||
self.assertEqual(domain_information.city, "Fudge")
|
||||
self.assertEqual(domain_information_election.city, "Caramel")
|
||||
|
||||
# Test for non-election
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for election
|
||||
self.assertEqual(
|
||||
domain_information_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_information_election.is_election_board, True)
|
||||
self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
|
|
@ -591,7 +591,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
||||
"Special district,School district,Election office\n"
|
||||
"3,2,1,0,0,0,0,0,0,2\n"
|
||||
"3,2,1,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"Domain name,Domain type,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
||||
|
@ -718,7 +718,12 @@ class HelperFunctions(MockDb):
|
|||
}
|
||||
# Test with distinct
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 2]
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||
|
||||
# Test without distinct
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||
|
||||
def test_get_sliced_requests(self):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from registrar.models import Contact
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue