Merge branch 'main' into hotgov/2380-rearrange-status-fields

This commit is contained in:
zandercymatics 2024-07-03 08:08:38 -06:00
commit fd8607c82f
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
62 changed files with 1214 additions and 665 deletions

View file

@ -0,0 +1,47 @@
# 26. Django Waffle library for Feature Flags
Date: 2024-07-06
## Status
Approved
## Context
We release finished code twice weekly, allowing features to reach users quickly. However, several upcoming features require a series of changes that will need to be done over a few sprints and should only be displayed to users once we are all done. Thus, users would see half-finished features if we followed our standard process.
At the same time, some of these features should only be turned on for users upon request (and likely during user research). We would want a way for our CISA users to turn this feature on and off for people without requiring a lengthy process or code changes.
This brought us to finding solutions that could fix one or both of these problems.
## Considered Options
**Option 1:** Environment variables
The environment allows developers to set a true or false value to the given variable, allowing implementation over multiple sprints when new features are encapsulated with this variable. The feature shows when the variable is on (true); otherwise, it remains hidden. Environment variables are also innate to Django, making them free to use; on top of that, we already use them for other things in our code.
The downside is that you would need to go to cloud.gov or use the cf CLI to see the current settings on a sandbox. This is very technical, meaning only developers would really be able to see what features were set, and we would be the only ones able to adjust them. It would also be easy to accidentally have the feature on or off without noticing. This also would not solve the problem of turning features on and off quickly for a given user group.
**Option 2:** Feature branches
Like environment variables, using feature branches would be free and allow us to iterate on developing big features over multiple sprints. We would make a feature branch that developers working on that feature would push and pull from to iterate on. This quickly brings us to the downsides of this approach.
Using feature branches, we do not solve the problem of being able to turn features on and off quickly for a user group. More importantly, by working in a separate branch for more than a sprint, we easily risk having out-of-sync migrations and merge conflicts that would slow development time and cause frustration. Out-of-sync migrations can also cause technical issues on sandboxes, further contributing to development frustration.
**Option 3:** Feature flags
Feature flags are free, allowing us to implement features over multiple sprints, and some libraries can apply features based on UserGroups while even more come with an interface for non-developers to control turning feature flags on and off. Going with this decision would also entail picking the correct library or product.
**Option 3a:** Feature flags with Waffle
The Waffle feature flag library is a highly recommended Django library for handling large features. It has clear documentation on turning on feature flags for user groups, which is one of the main problems it attempts to solve. It also provides "Samples" that can turn on flags for a certain percentage of users and "Switches" that can be used to turn features on and off holistically. The reviews from those who used it were highly favorable, some even mentioning how it beat out competitors like Gargoyl. It's also compatible with Django admin, providing a quick way to add the view of the flags in Django admin so any user with admin access can modify flags for their sandbox.
The repo has had new releases every year since its the creation and looks to be well maintained, with many issues on the repo referring to new feature requests.
**Option 3b:** Feature flags with Gargoyl
Gargoyl is another feature-flag library with Django, but it is no longer maintained, and reviews say it wasn't as easy to work with as Waffle. Using it would require forking the library, and many outstanding issues indicate bugs that need fixing. The mixed reviews from those who have done this and the less robust documentation were immediately huge cons to using this as an option.
**Option 3c:** Paid feature flag system with GitHub integration- LaunchDarkly
LaunchDarkly is a Fedramped solution with excellent reviews for controlling feature flags straight from GitHub to promote any team member easily controlling feature flags. However, the big con to this was that it would be a paid solution and would take time to procure, thus slowing down our ability to start on these significant features. We shouldn't consider LaunchDarkly because taking time to procure it would negatively affect our timeline, even if the budget was eventually approved.
## Decision
Option 3a, feature flags with the Django Waffle library
## Consequences
We are now reliant on the Waffle library for feature flags. As with any library, we would need to fork it if it ever became non-maintained with critical bugs. This doesn't seem likely in the near future, but if it occurred, we could complete the forking and fix any bug within a sprint without drastically impacting our timeline.

View file

@ -41,7 +41,7 @@ class DomainRequest {
-- --
creator (User) creator (User)
investigator (User) investigator (User)
authorizing_official (Contact) senior_official (Contact)
submitter (Contact) submitter (Contact)
other_contacts (Contacts) other_contacts (Contacts)
approved_domain (Domain) approved_domain (Domain)
@ -80,7 +80,7 @@ class Contact {
-- --
} }
DomainRequest *-r-* Contact : authorizing_official, submitter, other_contacts DomainRequest *-r-* Contact : senior_official, submitter, other_contacts
class DraftDomain { class DraftDomain {
Requested domain Requested domain

View file

@ -20,7 +20,7 @@ docker compose exec app ./manage.py generate_puml --include registrar
## How To regenerate the database svg image ## How To regenerate the database svg image
1. Copy your puml file contents into the bottom of this file and replace the current code marked by `plantuml` 1. Copy your puml file contents into the bottom of this file and replace the current code marked by `plantuml`
2. Run the following command 2. Navigate to the `diagram` folder and then run the following command below:
```bash ```bash
docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md
@ -103,6 +103,21 @@ class "registrar.PublicContact <Registrar>" as registrar.PublicContact #d6f4e9 {
registrar.PublicContact -- registrar.Domain registrar.PublicContact -- registrar.Domain
class "registrar.UserDomainRole <Registrar>" as registrar.UserDomainRole #d6f4e9 {
user domain role
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
~ user (ForeignKey)
~ domain (ForeignKey)
+ role (TextField)
--
}
registrar.UserDomainRole -- registrar.User
registrar.UserDomainRole -- registrar.Domain
class "registrar.Domain <Registrar>" as registrar.Domain #d6f4e9 { class "registrar.Domain <Registrar>" as registrar.Domain #d6f4e9 {
domain domain
-- --
@ -115,6 +130,7 @@ class "registrar.Domain <Registrar>" as registrar.Domain #d6f4e9 {
+ security_contact_registry_id (TextField) + security_contact_registry_id (TextField)
+ deleted (DateField) + deleted (DateField)
+ first_ready (DateField) + first_ready (DateField)
+ dsdata_last_change (TextField)
-- --
} }
@ -126,6 +142,7 @@ class "registrar.FederalAgency <Registrar>" as registrar.FederalAgency #d6f4e9 {
+ created_at (DateTimeField) + created_at (DateTimeField)
+ updated_at (DateTimeField) + updated_at (DateTimeField)
+ agency (CharField) + agency (CharField)
+ federal_type (CharField)
-- --
} }
@ -138,7 +155,10 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
+ updated_at (DateTimeField) + updated_at (DateTimeField)
+ status (FSMField) + status (FSMField)
+ rejection_reason (TextField) + rejection_reason (TextField)
+ action_needed_reason (TextField)
+ action_needed_reason_email (TextField)
~ federal_agency (ForeignKey) ~ federal_agency (ForeignKey)
~ portfolio (ForeignKey)
~ creator (ForeignKey) ~ creator (ForeignKey)
~ investigator (ForeignKey) ~ investigator (ForeignKey)
+ generic_org_type (CharField) + generic_org_type (CharField)
@ -156,7 +176,7 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
+ zipcode (CharField) + zipcode (CharField)
+ urbanization (CharField) + urbanization (CharField)
+ about_your_organization (TextField) + about_your_organization (TextField)
~ authorizing_official (ForeignKey) ~ senior_official (ForeignKey)
~ approved_domain (OneToOneField) ~ approved_domain (OneToOneField)
~ requested_domain (OneToOneField) ~ requested_domain (OneToOneField)
~ submitter (ForeignKey) ~ submitter (ForeignKey)
@ -165,6 +185,8 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
+ anything_else (TextField) + anything_else (TextField)
+ has_anything_else_text (BooleanField) + has_anything_else_text (BooleanField)
+ cisa_representative_email (EmailField) + cisa_representative_email (EmailField)
+ cisa_representative_first_name (CharField)
+ cisa_representative_last_name (CharField)
+ has_cisa_representative (BooleanField) + has_cisa_representative (BooleanField)
+ is_policy_acknowledged (BooleanField) + is_policy_acknowledged (BooleanField)
+ submission_date (DateField) + submission_date (DateField)
@ -175,6 +197,7 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
-- --
} }
registrar.DomainRequest -- registrar.FederalAgency registrar.DomainRequest -- registrar.FederalAgency
registrar.DomainRequest -- registrar.Portfolio
registrar.DomainRequest -- registrar.User registrar.DomainRequest -- registrar.User
registrar.DomainRequest -- registrar.User registrar.DomainRequest -- registrar.User
registrar.DomainRequest -- registrar.Contact registrar.DomainRequest -- registrar.Contact
@ -194,6 +217,7 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
+ updated_at (DateTimeField) + updated_at (DateTimeField)
~ federal_agency (ForeignKey) ~ federal_agency (ForeignKey)
~ creator (ForeignKey) ~ creator (ForeignKey)
~ portfolio (ForeignKey)
~ domain_request (OneToOneField) ~ domain_request (OneToOneField)
+ generic_org_type (CharField) + generic_org_type (CharField)
+ organization_type (CharField) + organization_type (CharField)
@ -210,13 +234,17 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
+ zipcode (CharField) + zipcode (CharField)
+ urbanization (CharField) + urbanization (CharField)
+ about_your_organization (TextField) + about_your_organization (TextField)
~ authorizing_official (ForeignKey) ~ senior_official (ForeignKey)
~ domain (OneToOneField) ~ domain (OneToOneField)
~ submitter (ForeignKey) ~ submitter (ForeignKey)
+ purpose (TextField) + purpose (TextField)
+ no_other_contacts_rationale (TextField) + no_other_contacts_rationale (TextField)
+ anything_else (TextField) + anything_else (TextField)
+ has_anything_else_text (BooleanField)
+ cisa_representative_email (EmailField) + cisa_representative_email (EmailField)
+ cisa_representative_first_name (CharField)
+ cisa_representative_last_name (CharField)
+ has_cisa_representative (BooleanField)
+ is_policy_acknowledged (BooleanField) + is_policy_acknowledged (BooleanField)
+ notes (TextField) + notes (TextField)
# other_contacts (ManyToManyField) # other_contacts (ManyToManyField)
@ -224,6 +252,7 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
} }
registrar.DomainInformation -- registrar.FederalAgency registrar.DomainInformation -- registrar.FederalAgency
registrar.DomainInformation -- registrar.User registrar.DomainInformation -- registrar.User
registrar.DomainInformation -- registrar.Portfolio
registrar.DomainInformation -- registrar.DomainRequest registrar.DomainInformation -- registrar.DomainRequest
registrar.DomainInformation -- registrar.Contact registrar.DomainInformation -- registrar.Contact
registrar.DomainInformation -- registrar.Domain registrar.DomainInformation -- registrar.Domain
@ -242,21 +271,6 @@ class "registrar.DraftDomain <Registrar>" as registrar.DraftDomain #d6f4e9 {
} }
class "registrar.UserDomainRole <Registrar>" as registrar.UserDomainRole #d6f4e9 {
user domain role
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
~ user (ForeignKey)
~ domain (ForeignKey)
+ role (TextField)
--
}
registrar.UserDomainRole -- registrar.User
registrar.UserDomainRole -- registrar.Domain
class "registrar.DomainInvitation <Registrar>" as registrar.DomainInvitation #d6f4e9 { class "registrar.DomainInvitation <Registrar>" as registrar.DomainInvitation #d6f4e9 {
domain invitation domain invitation
-- --
@ -388,6 +402,58 @@ class "registrar.WaffleFlag <Registrar>" as registrar.WaffleFlag #d6f4e9 {
registrar.WaffleFlag *--* registrar.User registrar.WaffleFlag *--* registrar.User
class "registrar.Portfolio <Registrar>" as registrar.Portfolio #d6f4e9 {
portfolio
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
~ creator (ForeignKey)
+ notes (TextField)
~ federal_agency (ForeignKey)
+ organization_type (CharField)
+ organization_name (CharField)
+ address_line1 (CharField)
+ address_line2 (CharField)
+ city (CharField)
+ state_territory (CharField)
+ zipcode (CharField)
+ urbanization (CharField)
+ security_contact_email (EmailField)
--
}
registrar.Portfolio -- registrar.User
registrar.Portfolio -- registrar.FederalAgency
class "registrar.DomainGroup <Registrar>" as registrar.DomainGroup #d6f4e9 {
domain group
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
+ name (CharField)
~ portfolio (ForeignKey)
# domains (ManyToManyField)
--
}
registrar.DomainGroup -- registrar.Portfolio
registrar.DomainGroup *--* registrar.DomainInformation
class "registrar.Suborganization <Registrar>" as registrar.Suborganization #d6f4e9 {
suborganization
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
+ name (CharField)
~ portfolio (ForeignKey)
--
}
registrar.Suborganization -- registrar.Portfolio
@enduml @enduml
``` ```

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Before After
Before After

View file

@ -11,7 +11,7 @@
"http://localhost:8080/request/org_federal/", "http://localhost:8080/request/org_federal/",
"http://localhost:8080/request/org_election/", "http://localhost:8080/request/org_election/",
"http://localhost:8080/request/org_contact/", "http://localhost:8080/request/org_contact/",
"http://localhost:8080/request/authorizing_official/", "http://localhost:8080/request/senior_official/",
"http://localhost:8080/request/current_sites/", "http://localhost:8080/request/current_sites/",
"http://localhost:8080/request/dotgov_domain/", "http://localhost:8080/request/dotgov_domain/",
"http://localhost:8080/request/purpose/", "http://localhost:8080/request/purpose/",

View file

@ -67,8 +67,8 @@ services:
# command: "python" # command: "python"
command: > command: >
bash -c " python manage.py migrate && bash -c " python manage.py migrate &&
python manage.py load &&
python manage.py createcachetable && python manage.py createcachetable &&
python manage.py load &&
python manage.py runserver 0.0.0.0:8080" python manage.py runserver 0.0.0.0:8080"
db: db:

View file

@ -15,7 +15,6 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from dateutil.relativedelta import relativedelta # type: ignore
from epplibwrapper.errors import ErrorCode, RegistryError from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin from waffle.admin import FlagAdmin
@ -449,7 +448,7 @@ class AdminSortFields:
sort_mapping = { sort_mapping = {
# == Contact == # # == Contact == #
"other_contacts": (Contact, _name_sort), "other_contacts": (Contact, _name_sort),
"authorizing_official": (Contact, _name_sort), "senior_official": (Contact, _name_sort),
"submitter": (Contact, _name_sort), "submitter": (Contact, _name_sort),
# == User == # # == User == #
"creator": (User, _name_sort), "creator": (User, _name_sort),
@ -603,6 +602,27 @@ class UserContactInline(admin.StackedInline):
model = models.Contact model = models.Contact
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
"user",
"email",
]
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
We have 1 conditions that determine which fields are read-only:
admin user permissions.
"""
readonly_fields = list(self.readonly_fields)
if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields # Read-only fields for analysts
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
"""Custom user admin class to use our inlines.""" """Custom user admin class to use our inlines."""
@ -650,7 +670,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
None, None,
{"fields": ("username", "password", "status", "verification_type")}, {"fields": ("username", "password", "status", "verification_type")},
), ),
("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
( (
"Permissions", "Permissions",
{ {
@ -681,7 +701,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
) )
}, },
), ),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
( (
"Permissions", "Permissions",
{ {
@ -705,7 +725,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
# NOT all fields are readonly for admin, otherwise we would have # NOT all fields are readonly for admin, otherwise we would have
# set this at the permissions level. The exception is 'status' # set this at the permissions level. The exception is 'status'
analyst_readonly_fields = [ analyst_readonly_fields = [
"Personal Info", "User profile",
"first_name", "first_name",
"middle_name", "middle_name",
"last_name", "last_name",
@ -942,6 +962,7 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Read only that we'll leverage for CISA Analysts # Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [ analyst_readonly_fields = [
"user", "user",
"email",
] ]
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
@ -1238,9 +1259,9 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
search_help_text = "Search by domain." search_help_text = "Search by domain."
fieldsets = [ fieldsets = [
(None, {"fields": ["portfolio", "creator", "submitter", "domain_request", "notes"]}), (None, {"fields": ["portfolio", "sub_organization", "creator", "submitter", "domain_request", "notes"]}),
(".gov domain", {"fields": ["domain"]}), (".gov domain", {"fields": ["domain"]}),
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), ("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
("Background info", {"fields": ["anything_else"]}), ("Background info", {"fields": ["anything_else"]}),
( (
"Type of organization", "Type of organization",
@ -1314,9 +1335,11 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
autocomplete_fields = [ autocomplete_fields = [
"creator", "creator",
"domain_request", "domain_request",
"authorizing_official", "senior_official",
"domain", "domain",
"submitter", "submitter",
"portfolio",
"sub_organization",
] ]
# Table ordering # Table ordering
@ -1326,6 +1349,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
superuser_only_fields = [ superuser_only_fields = [
"portfolio", "portfolio",
"sub_organization",
] ]
# DEVELOPER's NOTE: # DEVELOPER's NOTE:
@ -1521,6 +1545,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
{ {
"fields": [ "fields": [
"portfolio", "portfolio",
"sub_organization",
"status_history", "status_history",
"status", "status",
"rejection_reason", "rejection_reason",
@ -1539,7 +1564,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"Contacts", "Contacts",
{ {
"fields": [ "fields": [
"authorizing_official", "senior_official",
"other_contacts", "other_contacts",
"no_other_contacts_rationale", "no_other_contacts_rationale",
"cisa_representative_first_name", "cisa_representative_first_name",
@ -1630,13 +1655,16 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"requested_domain", "requested_domain",
"submitter", "submitter",
"creator", "creator",
"authorizing_official", "senior_official",
"investigator", "investigator",
"portfolio",
"sub_organization",
] ]
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
superuser_only_fields = [ superuser_only_fields = [
"portfolio", "portfolio",
"sub_organization",
] ]
# DEVELOPER's NOTE: # DEVELOPER's NOTE:
@ -2056,14 +2084,7 @@ class DomainInformationInline(admin.StackedInline):
fieldsets = DomainInformationAdmin.fieldsets fieldsets = DomainInformationAdmin.fieldsets
readonly_fields = DomainInformationAdmin.readonly_fields readonly_fields = DomainInformationAdmin.readonly_fields
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
autocomplete_fields = DomainInformationAdmin.autocomplete_fields
autocomplete_fields = [
"creator",
"domain_request",
"authorizing_official",
"domain",
"submitter",
]
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
"""Custom has_change_permission override so that we can specify that """Custom has_change_permission override so that we can specify that
@ -2175,8 +2196,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
), ),
) )
# this ordering effects the ordering of results # this ordering effects the ordering of results in autocomplete_fields for domain
# in autocomplete_fields for domain
ordering = ["name"] ordering = ["name"]
def generic_org_type(self, obj): def generic_org_type(self, obj):
@ -2258,25 +2278,12 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
extra_context["state_help_message"] = Domain.State.get_admin_help_text(domain.state) extra_context["state_help_message"] = Domain.State.get_admin_help_text(domain.state)
extra_context["domain_state"] = domain.get_state_display() extra_context["domain_state"] = domain.get_state_display()
extra_context["curr_exp_date"] = (
# Pass in what the an extended expiration date would be for the expiration date modal domain.expiration_date if domain.expiration_date is not None else self._get_current_date()
self._set_expiration_date_context(domain, extra_context) )
return super().changeform_view(request, object_id, form_url, extra_context) return super().changeform_view(request, object_id, form_url, extra_context)
def _set_expiration_date_context(self, domain, extra_context):
"""Given a domain, calculate the an extended expiration date
from the current registry expiration date."""
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
try:
curr_exp_date = domain.registry_expiration_date
except KeyError:
# No expiration date was found. Return none.
extra_context["extended_expiration_date"] = None
else:
new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
extra_context["extended_expiration_date"] = new_date
def response_change(self, request, obj): def response_change(self, request, obj):
# Create dictionary of action functions # Create dictionary of action functions
ACTION_FUNCTIONS = { ACTION_FUNCTIONS = {
@ -2304,11 +2311,9 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
self.message_user(request, "Object is not of type Domain.", messages.ERROR) self.message_user(request, "Object is not of type Domain.", messages.ERROR)
return None return None
years = self._get_calculated_years_for_exp_date(obj)
# Renew the domain. # Renew the domain.
try: try:
obj.renew_domain(length=years) obj.renew_domain()
self.message_user( self.message_user(
request, request,
"Successfully extended the expiration date.", "Successfully extended the expiration date.",
@ -2333,37 +2338,6 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return HttpResponseRedirect(".") return HttpResponseRedirect(".")
def _get_calculated_years_for_exp_date(self, obj, extension_period: int = 1):
"""Given the current date, an extension period, and a registry_expiration_date
on the domain object, calculate the number of years needed to extend the
current expiration date by the extension period.
"""
# Get the date we want to update to
desired_date = self._get_current_date() + relativedelta(years=extension_period)
# Grab the current expiration date
try:
exp_date = obj.registry_expiration_date
except KeyError:
# if no expiration date from registry, set it to today
logger.warning("current expiration date not set; setting to today")
exp_date = self._get_current_date()
# If the expiration date is super old (2020, for example), we need to
# "catch up" to the current year, so we add the difference.
# If both years match, then lets just proceed as normal.
calculated_exp_date = exp_date + relativedelta(years=extension_period)
year_difference = desired_date.year - exp_date.year
years = extension_period
if desired_date > calculated_exp_date:
# Max probably isn't needed here (no code flow), but it guards against negative and 0.
# In both of those cases, we just want to extend by the extension_period.
years = max(extension_period, year_difference)
return years
# Workaround for unit tests, as we cannot mock date directly. # Workaround for unit tests, as we cannot mock date directly.
# it is immutable. Rather than dealing with a convoluted workaround, # it is immutable. Rather than dealing with a convoluted workaround,
# lets wrap this in a function. # lets wrap this in a function.
@ -2706,6 +2680,11 @@ class PortfolioAdmin(ListHeaderAdmin):
# readonly_fields = [ # readonly_fields = [
# "requestor", # "requestor",
# ] # ]
# Creates select2 fields (with search bars)
autocomplete_fields = [
"creator",
"federal_agency",
]
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
@ -2789,6 +2768,10 @@ class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
list_display = ["name", "portfolio"] list_display = ["name", "portfolio"]
autocomplete_fields = [
"portfolio",
]
search_fields = ["name"]
admin.site.unregister(LogEntry) # Unregister the default registration admin.site.unregister(LogEntry) # Unregister the default registration

View file

@ -46,7 +46,7 @@ function ScrollToElement(attributeName, attributeValue) {
} else if (attributeName === 'id') { } else if (attributeName === 'id') {
targetEl = document.getElementById(attributeValue); targetEl = document.getElementById(attributeValue);
} else { } else {
console.log('Error: unknown attribute name provided.'); console.error('Error: unknown attribute name provided.');
return; // Exit the function if an invalid attributeName is provided return; // Exit the function if an invalid attributeName is provided
} }
@ -78,6 +78,50 @@ function makeVisible(el) {
el.style.visibility = "visible"; el.style.visibility = "visible";
} }
/**
* Toggles expand_more / expand_more svgs in buttons or anchors
* @param {Element} element - DOM element
*/
function toggleCaret(element) {
// Get a reference to the use element inside the button
const useElement = element.querySelector('use');
// Check if the span element text is 'Hide'
if (useElement.getAttribute('xlink:href') === '/public/img/sprite.svg#expand_more') {
// Update the xlink:href attribute to expand_more
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
} else {
// Update the xlink:href attribute to expand_less
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
}
}
/**
* Helper function that scrolls to an element
* @param {string} attributeName - The string "class" or "id"
* @param {string} attributeValue - The class or id name
*/
function ScrollToElement(attributeName, attributeValue) {
let targetEl = null;
if (attributeName === 'class') {
targetEl = document.getElementsByClassName(attributeValue)[0];
} else if (attributeName === 'id') {
targetEl = document.getElementById(attributeValue);
} else {
console.error('Error: unknown attribute name provided.');
return; // Exit the function if an invalid attributeName is provided
}
if (targetEl) {
const rect = targetEl.getBoundingClientRect();
const scrollTop = window.scrollY || document.documentElement.scrollTop;
window.scrollTo({
top: rect.top + scrollTop,
behavior: 'smooth' // Optional: for smooth scrolling
});
}
}
/** Creates and returns a live region element. */ /** Creates and returns a live region element. */
function createLiveRegion(id) { function createLiveRegion(id) {
const liveRegion = document.createElement("div"); const liveRegion = document.createElement("div");
@ -927,7 +971,7 @@ function unloadModals() {
* @param {string} itemName - The name displayed in the counter * @param {string} itemName - The name displayed in the counter
* @param {string} paginationSelector - CSS selector for the pagination container. * @param {string} paginationSelector - CSS selector for the pagination container.
* @param {string} counterSelector - CSS selector for the pagination counter. * @param {string} counterSelector - CSS selector for the pagination counter.
* @param {string} headerAnchor - CSS selector for the header element to anchor the links to. * @param {string} linkAnchor - CSS selector for the header element to anchor the links to.
* @param {Function} loadPageFunction - Function to call when a page link is clicked. * @param {Function} loadPageFunction - Function to call when a page link is clicked.
* @param {number} currentPage - The current page number (starting with 1). * @param {number} currentPage - The current page number (starting with 1).
* @param {number} numPages - The total number of pages. * @param {number} numPages - The total number of pages.
@ -936,7 +980,7 @@ function unloadModals() {
* @param {number} totalItems - The total number of items. * @param {number} totalItems - The total number of items.
* @param {string} searchTerm - The search term * @param {string} searchTerm - The search term
*/ */
function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) { function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
const paginationContainer = document.querySelector(paginationSelector); const paginationContainer = document.querySelector(paginationSelector);
const paginationCounter = document.querySelector(counterSelector); const paginationCounter = document.querySelector(counterSelector);
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
@ -955,7 +999,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
const prevPageItem = document.createElement('li'); const prevPageItem = document.createElement('li');
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
prevPageItem.innerHTML = ` prevPageItem.innerHTML = `
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page"> <a href="${linkAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
<svg class="usa-icon" aria-hidden="true" role="img"> <svg class="usa-icon" aria-hidden="true" role="img">
<use xlink:href="/public/img/sprite.svg#navigate_before"></use> <use xlink:href="/public/img/sprite.svg#navigate_before"></use>
</svg> </svg>
@ -974,7 +1018,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
const pageItem = document.createElement('li'); const pageItem = document.createElement('li');
pageItem.className = 'usa-pagination__item usa-pagination__page-no'; pageItem.className = 'usa-pagination__item usa-pagination__page-no';
pageItem.innerHTML = ` pageItem.innerHTML = `
<a href="${headerAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a> <a href="${linkAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
`; `;
if (page === currentPage) { if (page === currentPage) {
pageItem.querySelector('a').classList.add('usa-current'); pageItem.querySelector('a').classList.add('usa-current');
@ -1020,7 +1064,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
const nextPageItem = document.createElement('li'); const nextPageItem = document.createElement('li');
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
nextPageItem.innerHTML = ` nextPageItem.innerHTML = `
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page"> <a href="${linkAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
<span class="usa-pagination__link-text">Next</span> <span class="usa-pagination__link-text">Next</span>
<svg class="usa-icon" aria-hidden="true" role="img"> <svg class="usa-icon" aria-hidden="true" role="img">
<use xlink:href="/public/img/sprite.svg#navigate_next"></use> <use xlink:href="/public/img/sprite.svg#navigate_next"></use>
@ -1039,20 +1083,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
* A helper that toggles content/ no content/ no search results * A helper that toggles content/ no content/ no search results
* *
*/ */
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm) => { const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
const { unfiltered_total, total } = data; const { unfiltered_total, total } = data;
if (searchTermHolder)
searchTermHolder.innerHTML = '';
if (unfiltered_total) { if (unfiltered_total) {
if (total) { if (total) {
showElement(dataWrapper); showElement(dataWrapper);
hideElement(noSearchResultsWrapper); hideElement(noSearchResultsWrapper);
hideElement(noDataWrapper); hideElement(noDataWrapper);
} else { } else {
if (searchTermHolder)
searchTermHolder.innerHTML = currentSearchTerm;
hideElement(dataWrapper); hideElement(dataWrapper);
showElement(noSearchResultsWrapper); showElement(noSearchResultsWrapper);
hideElement(noDataWrapper); hideElement(noDataWrapper);
@ -1090,14 +1128,18 @@ document.addEventListener('DOMContentLoaded', function() {
let currentOrder = 'asc'; let currentOrder = 'asc';
const noDomainsWrapper = document.querySelector('.domains__no-data'); const noDomainsWrapper = document.querySelector('.domains__no-data');
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results'); const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
let hasLoaded = false; let scrollToTable = false;
let currentSearchTerm = '' let currentStatus = [];
let currentSearchTerm = '';
const domainsSearchInput = document.getElementById('domains__search-field'); const domainsSearchInput = document.getElementById('domains__search-field');
const domainsSearchSubmit = document.getElementById('domains__search-field-submit'); const domainsSearchSubmit = document.getElementById('domains__search-field-submit');
const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]'); const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region'); const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region');
const searchTermHolder = document.querySelector('.domains__search-term'); const resetSearchButton = document.querySelector('.domains__reset-search');
const resetButton = document.querySelector('.domains__reset-button'); const resetFiltersButton = document.querySelector('.domains__reset-filters');
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
const statusIndicator = document.querySelector('.domain__filter-indicator');
const statusToggle = document.querySelector('.usa-button--filter');
/** /**
* Loads rows in the domains list, as well as updates pagination around the domains list * Loads rows in the domains list, as well as updates pagination around the domains list
@ -1105,21 +1147,21 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} page - the page number of the results (starts with 1) * @param {*} page - the page number of the results (starts with 1)
* @param {*} sortBy - the sort column option * @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc} * @param {*} order - the sort order {asc, desc}
* @param {*} loaded - control for the scrollToElement functionality * @param {*} scroll - control for the scrollToElement functionality
* @param {*} searchTerm - the search term * @param {*} searchTerm - the search term
*/ */
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) { function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm) {
//fetch json of page of domains, given page # and sort // fetch json of page of domains, given params
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`) fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.error) { if (data.error) {
console.log('Error in AJAX call: ' + data.error); console.error('Error in AJAX call: ' + data.error);
return; return;
} }
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results // handle the display of proper messaging in the event that no domains exist in the list or search returns no results
updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm); updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, currentSearchTerm);
// identify the DOM element where the domain list will be inserted into the DOM // identify the DOM element where the domain list will be inserted into the DOM
const domainList = document.querySelector('.domains__table tbody'); const domainList = document.querySelector('.domains__table tbody');
@ -1132,7 +1174,6 @@ document.addEventListener('DOMContentLoaded', function() {
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : ''; const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
const actionUrl = domain.action_url; const actionUrl = domain.action_url;
const row = document.createElement('tr'); const row = document.createElement('tr');
row.innerHTML = ` row.innerHTML = `
<th scope="row" role="rowheader" data-label="Domain name"> <th scope="row" role="rowheader" data-label="Domain name">
@ -1148,7 +1189,7 @@ document.addEventListener('DOMContentLoaded', function() {
data-position="top" data-position="top"
title="${domain.get_state_help_text}" title="${domain.get_state_help_text}"
focusable="true" focusable="true"
aria-label="Status Information" aria-label="${domain.get_state_help_text}"
role="tooltip" role="tooltip"
> >
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use> <use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
@ -1169,16 +1210,16 @@ document.addEventListener('DOMContentLoaded', function() {
initializeTooltips(); initializeTooltips();
// Do not scroll on first page load // Do not scroll on first page load
if (loaded) if (scroll)
ScrollToElement('id', 'domains-header'); ScrollToElement('class', 'domains');
hasLoaded = true; scrollToTable = true;
// update pagination // update pagination
updatePagination( updatePagination(
'domain', 'domain',
'#domains-pagination', '#domains-pagination',
'#domains-pagination .usa-pagination__counter', '#domains-pagination .usa-pagination__counter',
'#domains-header', '#domains',
loadDomains, loadDomains,
data.page, data.page,
data.num_pages, data.num_pages,
@ -1214,13 +1255,51 @@ document.addEventListener('DOMContentLoaded', function() {
currentSearchTerm = domainsSearchInput.value; currentSearchTerm = domainsSearchInput.value;
// If the search is blank, we match the resetSearch functionality // If the search is blank, we match the resetSearch functionality
if (currentSearchTerm) { if (currentSearchTerm) {
showElement(resetButton); showElement(resetSearchButton);
} else { } else {
hideElement(resetButton); hideElement(resetSearchButton);
} }
loadDomains(1, 'id', 'asc'); loadDomains(1, 'id', 'asc');
resetHeaders(); resetHeaders();
}) });
if (statusToggle) {
statusToggle.addEventListener('click', function() {
toggleCaret(statusToggle);
});
}
// Add event listeners to status filter checkboxes
statusCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const checkboxValue = this.value;
// Update currentStatus array based on checkbox state
if (this.checked) {
currentStatus.push(checkboxValue);
} else {
const index = currentStatus.indexOf(checkboxValue);
if (index > -1) {
currentStatus.splice(index, 1);
}
}
// Manage visibility of reset filters button
if (currentStatus.length == 0) {
hideElement(resetFiltersButton);
} else {
showElement(resetFiltersButton);
}
// Disable the auto scroll
scrollToTable = false;
// Call loadDomains with updated status
loadDomains(1, 'id', 'asc');
resetHeaders();
updateStatusIndicator();
});
});
// Reset UI and accessibility // Reset UI and accessibility
function resetHeaders() { function resetHeaders() {
@ -1235,18 +1314,78 @@ document.addEventListener('DOMContentLoaded', function() {
function resetSearch() { function resetSearch() {
domainsSearchInput.value = ''; domainsSearchInput.value = '';
currentSearchTerm = ''; currentSearchTerm = '';
hideElement(resetButton); hideElement(resetSearchButton);
loadDomains(1, 'id', 'asc', hasLoaded, ''); loadDomains(1, 'id', 'asc');
resetHeaders(); resetHeaders();
} }
if (resetButton) { if (resetSearchButton) {
resetButton.addEventListener('click', function() { resetSearchButton.addEventListener('click', function() {
resetSearch(); resetSearch();
}); });
} }
// Load the first page initially function resetFilters() {
currentStatus = [];
statusCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
hideElement(resetFiltersButton);
// Disable the auto scroll
scrollToTable = false;
loadDomains(1, 'id', 'asc');
resetHeaders();
updateStatusIndicator();
// No need to toggle close the filters. The focus shift will trigger that for us.
}
if (resetFiltersButton) {
resetFiltersButton.addEventListener('click', function() {
resetFilters();
});
}
function updateStatusIndicator() {
statusIndicator.innerHTML = '';
// Even if the element is empty, it'll mess up the flex layout unless we set display none
statusIndicator.hideElement();
if (currentStatus.length)
statusIndicator.innerHTML = '(' + currentStatus.length + ')';
statusIndicator.showElement();
}
function closeFilters() {
if (statusToggle.getAttribute("aria-expanded") === "true") {
statusToggle.click();
}
}
// Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter,
// user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button.
// NOTE: We may need to evolve this as we add more filters.
document.addEventListener('focusin', function(event) {
const accordion = document.querySelector('.usa-accordion--select');
const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
if (accordionIsOpen && !accordion.contains(event.target)) {
closeFilters();
}
});
// Close when user clicks outside
// NOTE: We may need to evolve this as we add more filters.
document.addEventListener('click', function(event) {
const accordion = document.querySelector('.usa-accordion--select');
const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
if (accordionIsOpen && !accordion.contains(event.target)) {
closeFilters();
}
});
// Initial load
loadDomains(1); loadDomains(1);
} }
}); });
@ -1279,14 +1418,13 @@ document.addEventListener('DOMContentLoaded', function() {
let currentOrder = 'asc'; let currentOrder = 'asc';
const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data'); const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results'); const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
let hasLoaded = false; let scrollToTable = false;
let currentSearchTerm = '' let currentSearchTerm = '';
const domainRequestsSearchInput = document.getElementById('domain-requests__search-field'); const domainRequestsSearchInput = document.getElementById('domain-requests__search-field');
const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit'); const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit');
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]'); const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region'); const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
const searchTermHolder = document.querySelector('.domain-requests__search-term'); const resetSearchButton = document.querySelector('.domain-requests__reset-search');
const resetButton = document.querySelector('.domain-requests__reset-button');
/** /**
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
@ -1316,7 +1454,7 @@ document.addEventListener('DOMContentLoaded', function() {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
// Update data and UI // Update data and UI
loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, hasLoaded, currentSearchTerm); loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, scrollToTable, currentSearchTerm);
}) })
.catch(error => console.error('Error fetching domain requests:', error)); .catch(error => console.error('Error fetching domain requests:', error));
} }
@ -1332,21 +1470,21 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} page - the page number of the results (starts with 1) * @param {*} page - the page number of the results (starts with 1)
* @param {*} sortBy - the sort column option * @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc} * @param {*} order - the sort order {asc, desc}
* @param {*} loaded - control for the scrollToElement functionality * @param {*} scroll - control for the scrollToElement functionality
* @param {*} searchTerm - the search term * @param {*} searchTerm - the search term
*/ */
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) { function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) {
//fetch json of page of domain requests, given page # and sort // fetch json of page of domain requests, given params
fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`) fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.error) { if (data.error) {
console.log('Error in AJAX call: ' + data.error); console.error('Error in AJAX call: ' + data.error);
return; return;
} }
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results // handle the display of proper messaging in the event that no requests exist in the list or search returns no results
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm); updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, currentSearchTerm);
// identify the DOM element where the domain request list will be inserted into the DOM // identify the DOM element where the domain request list will be inserted into the DOM
const tbody = document.querySelector('.domain-requests__table tbody'); const tbody = document.querySelector('.domain-requests__table tbody');
@ -1533,16 +1671,16 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
// Do not scroll on first page load // Do not scroll on first page load
if (loaded) if (scroll)
ScrollToElement('id', 'domain-requests-header'); ScrollToElement('class', 'domain-requests');
hasLoaded = true; scrollToTable = true;
// update the pagination after the domain requests list is updated // update the pagination after the domain requests list is updated
updatePagination( updatePagination(
'domain request', 'domain request',
'#domain-requests-pagination', '#domain-requests-pagination',
'#domain-requests-pagination .usa-pagination__counter', '#domain-requests-pagination .usa-pagination__counter',
'#domain-requests-header', '#domain-requests',
loadDomainRequests, loadDomainRequests,
data.page, data.page,
data.num_pages, data.num_pages,
@ -1577,13 +1715,13 @@ document.addEventListener('DOMContentLoaded', function() {
currentSearchTerm = domainRequestsSearchInput.value; currentSearchTerm = domainRequestsSearchInput.value;
// If the search is blank, we match the resetSearch functionality // If the search is blank, we match the resetSearch functionality
if (currentSearchTerm) { if (currentSearchTerm) {
showElement(resetButton); showElement(resetSearchButton);
} else { } else {
hideElement(resetButton); hideElement(resetSearchButton);
} }
loadDomainRequests(1, 'id', 'asc'); loadDomainRequests(1, 'id', 'asc');
resetHeaders(); resetHeaders();
}) });
// Reset UI and accessibility // Reset UI and accessibility
function resetHeaders() { function resetHeaders() {
@ -1598,24 +1736,23 @@ document.addEventListener('DOMContentLoaded', function() {
function resetSearch() { function resetSearch() {
domainRequestsSearchInput.value = ''; domainRequestsSearchInput.value = '';
currentSearchTerm = ''; currentSearchTerm = '';
hideElement(resetButton); hideElement(resetSearchButton);
loadDomainRequests(1, 'id', 'asc', hasLoaded, ''); loadDomainRequests(1, 'id', 'asc');
resetHeaders(); resetHeaders();
} }
if (resetButton) { if (resetSearchButton) {
resetButton.addEventListener('click', function() { resetSearchButton.addEventListener('click', function() {
resetSearch(); resetSearch();
}); });
} }
// Load the first page initially // Initial load
loadDomainRequests(1); loadDomainRequests(1);
} }
}); });
/** /**
* An IIFE that displays confirmation modal on the user profile page * An IIFE that displays confirmation modal on the user profile page
*/ */

View file

@ -0,0 +1,33 @@
@use "uswds-core" as *;
.usa-accordion--select {
display: inline-block;
width: auto;
position: relative;
.usa-accordion__button[aria-expanded=false],
.usa-accordion__button[aria-expanded=false]:hover,
.usa-accordion__button[aria-expanded=true],
.usa-accordion__button[aria-expanded=true]:hover {
background-image: none;
}
.usa-accordion__content {
// Note, width is determined by a custom width class on one of the children
position: absolute;
z-index: 1;
top: 33.88px;
left: 0;
border-radius: 4px;
border: solid 1px color('base-lighter');
padding: units(2) units(2) units(3) units(2);
width: max-content;
}
h2 {
font-size: size('body', 'sm');
}
.usa-button {
width: 100%;
}
.margin-top-0 {
margin-top: 0 !important;
}
}

View file

@ -83,6 +83,10 @@ body {
padding: 0 units(2) units(3); padding: 0 units(2) units(3);
margin-top: units(3); margin-top: units(3);
&.margin-top-0 {
margin-top: 0;
}
h2 { h2 {
color: color('primary-dark'); color: color('primary-dark');
margin-top: units(2); margin-top: units(2);
@ -96,6 +100,10 @@ body {
@include at-media(mobile-lg) { @include at-media(mobile-lg) {
margin-top: units(5); margin-top: units(5);
&.margin-top-0 {
margin-top: 0;
}
h2 { h2 {
margin-bottom: 0; margin-bottom: 0;
} }
@ -211,3 +219,7 @@ abbr[title] {
.usa-logo button.usa-button--unstyled.disabled-button:hover{ .usa-logo button.usa-button--unstyled.disabled-button:hover{
color: #{$dhs-dark-gray-85}; color: #{$dhs-dark-gray-85};
} }
.padding--8-8-9 {
padding: 8px 8px 9px !important;
}

View file

@ -161,3 +161,19 @@ a.usa-button--unstyled:visited {
margin-left: units(2); margin-left: units(2);
} }
} }
.usa-button--filter {
width: auto;
// For mobile stacking
margin-bottom: units(1);
border: solid 1px color('base-light') !important;
padding: units(1);
color: color('primary-darker') !important;
font-weight: font-weight('normal');
font-size: size('ui', 'xs');
box-shadow: none;
&:hover {
box-shadow: none;
}
}

View file

@ -27,7 +27,6 @@
} }
td .no-click-outline-and-cursor-help { td .no-click-outline-and-cursor-help {
outline: none;
cursor: help; cursor: help;
use { use {
// USWDS has weird interactions with SVGs regarding tooltips, // USWDS has weird interactions with SVGs regarding tooltips,

View file

@ -12,6 +12,7 @@
@forward "typography"; @forward "typography";
@forward "links"; @forward "links";
@forward "lists"; @forward "lists";
@forward "accordions";
@forward "buttons"; @forward "buttons";
@forward "pagination"; @forward "pagination";
@forward "forms"; @forward "forms";

View file

@ -44,7 +44,7 @@ for step, view in [
(Step.ORGANIZATION_ELECTION, views.OrganizationElection), (Step.ORGANIZATION_ELECTION, views.OrganizationElection),
(Step.ORGANIZATION_CONTACT, views.OrganizationContact), (Step.ORGANIZATION_CONTACT, views.OrganizationContact),
(Step.ABOUT_YOUR_ORGANIZATION, views.AboutYourOrganization), (Step.ABOUT_YOUR_ORGANIZATION, views.AboutYourOrganization),
(Step.AUTHORIZING_OFFICIAL, views.AuthorizingOfficial), (Step.SENIOR_OFFICIAL, views.SeniorOfficial),
(Step.CURRENT_SITES, views.CurrentSites), (Step.CURRENT_SITES, views.CurrentSites),
(Step.DOTGOV_DOMAIN, views.DotgovDomain), (Step.DOTGOV_DOMAIN, views.DotgovDomain),
(Step.PURPOSE, views.Purpose), (Step.PURPOSE, views.Purpose),
@ -183,9 +183,9 @@ urlpatterns = [
name="domain-org-name-address", name="domain-org-name-address",
), ),
path( path(
"domain/<int:pk>/authorizing-official", "domain/<int:pk>/senior-official",
views.DomainAuthorizingOfficialView.as_view(), views.DomainSeniorOfficialView.as_view(),
name="domain-authorizing-official", name="domain-senior-official",
), ),
path( path(
"domain/<int:pk>/security-email", "domain/<int:pk>/security-email",

View file

@ -36,7 +36,7 @@ class DomainRequestFixture:
# "purpose": None, # "purpose": None,
# "anything_else": None, # "anything_else": None,
# "is_policy_acknowledged": None, # "is_policy_acknowledged": None,
# "authorizing_official": None, # "senior_official": None,
# "submitter": None, # "submitter": None,
# "other_contacts": [], # "other_contacts": [],
# "current_websites": [], # "current_websites": [],
@ -117,11 +117,11 @@ class DomainRequestFixture:
if not da.investigator: if not da.investigator:
da.investigator = User.objects.get(username=user.username) if "investigator" in app else None da.investigator = User.objects.get(username=user.username) if "investigator" in app else None
if not da.authorizing_official: if not da.senior_official:
if "authorizing_official" in app and app["authorizing_official"] is not None: if "senior_official" in app and app["senior_official"] is not None:
da.authorizing_official, _ = Contact.objects.get_or_create(**app["authorizing_official"]) da.senior_official, _ = Contact.objects.get_or_create(**app["senior_official"])
else: else:
da.authorizing_official = Contact.objects.create(**cls.fake_contact()) da.senior_official = Contact.objects.create(**cls.fake_contact())
if not da.submitter: if not da.submitter:
if "submitter" in app and app["submitter"] is not None: if "submitter" in app and app["submitter"] is not None:

View file

@ -5,7 +5,7 @@ from .domain import (
DomainSecurityEmailForm, DomainSecurityEmailForm,
DomainOrgNameAddressForm, DomainOrgNameAddressForm,
ContactForm, ContactForm,
AuthorizingOfficialContactForm, SeniorOfficialContactForm,
DomainDnssecForm, DomainDnssecForm,
DomainDsdataFormset, DomainDsdataFormset,
DomainDsdataForm, DomainDsdataForm,

View file

@ -260,10 +260,10 @@ class ContactForm(forms.ModelForm):
self.domainInfo = domainInfo self.domainInfo = domainInfo
class AuthorizingOfficialContactForm(ContactForm): class SeniorOfficialContactForm(ContactForm):
"""Form for updating authorizing official contacts.""" """Form for updating senior official contacts."""
JOIN = "authorizing_official" JOIN = "senior_official"
def __init__(self, disable_fields=False, *args, **kwargs): def __init__(self, disable_fields=False, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -273,13 +273,13 @@ class AuthorizingOfficialContactForm(ContactForm):
# Set custom error messages # Set custom error messages
self.fields["first_name"].error_messages = { self.fields["first_name"].error_messages = {
"required": "Enter the first name / given name of your authorizing official." "required": "Enter the first name / given name of your senior official."
} }
self.fields["last_name"].error_messages = { self.fields["last_name"].error_messages = {
"required": "Enter the last name / family name of your authorizing official." "required": "Enter the last name / family name of your senior official."
} }
self.fields["title"].error_messages = { self.fields["title"].error_messages = {
"required": "Enter the title or role your authorizing official has in your \ "required": "Enter the title or role your senior official has in your \
organization (e.g., Chief Information Officer)." organization (e.g., Chief Information Officer)."
} }
self.fields["email"].error_messages = { self.fields["email"].error_messages = {
@ -306,21 +306,21 @@ class AuthorizingOfficialContactForm(ContactForm):
is_federal = self.domainInfo.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL is_federal = self.domainInfo.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
is_tribal = self.domainInfo.generic_org_type == DomainRequest.OrganizationChoices.TRIBAL is_tribal = self.domainInfo.generic_org_type == DomainRequest.OrganizationChoices.TRIBAL
# Get the Contact object from the db for the Authorizing Official # Get the Contact object from the db for the Senior Official
db_ao = Contact.objects.get(id=self.instance.id) db_so = Contact.objects.get(id=self.instance.id)
if (is_federal or is_tribal) and self.has_changed(): if (is_federal or is_tribal) and self.has_changed():
# This action should be blocked by the UI, as the text fields are readonly. # This action should be blocked by the UI, as the text fields are readonly.
# If they get past this point, we forbid it this way. # If they get past this point, we forbid it this way.
# This could be malicious, so lets reserve information for the backend only. # This could be malicious, so lets reserve information for the backend only.
raise ValueError("Authorizing Official cannot be modified for federal or tribal domains.") raise ValueError("Senior Official cannot be modified for federal or tribal domains.")
elif db_ao.has_more_than_one_join("information_authorizing_official"): elif db_so.has_more_than_one_join("information_senior_official"):
# Handle the case where the domain information object is available and the AO Contact # Handle the case where the domain information object is available and the SO Contact
# has more than one joined object. # has more than one joined object.
# In this case, create a new Contact, and update the new Contact with form data. # In this case, create a new Contact, and update the new Contact with form data.
# Then associate with domain information object as the authorizing_official # Then associate with domain information object as the senior_official
data = dict(self.cleaned_data.items()) data = dict(self.cleaned_data.items())
self.domainInfo.authorizing_official = Contact.objects.create(**data) self.domainInfo.senior_official = Contact.objects.create(**data)
self.domainInfo.save() self.domainInfo.save()
else: else:
# If all checks pass, just save normally # If all checks pass, just save normally

View file

@ -183,14 +183,14 @@ class AboutYourOrganizationForm(RegistrarForm):
) )
class AuthorizingOfficialForm(RegistrarForm): class SeniorOfficialForm(RegistrarForm):
JOIN = "authorizing_official" JOIN = "senior_official"
def to_database(self, obj): def to_database(self, obj):
if not self.is_valid(): if not self.is_valid():
return return
contact = getattr(obj, "authorizing_official", None) contact = getattr(obj, "senior_official", None)
if contact is not None and not contact.has_more_than_one_join("authorizing_official"): if contact is not None and not contact.has_more_than_one_join("senior_official"):
# if contact exists in the database and is not joined to other entities # if contact exists in the database and is not joined to other entities
super().to_database(contact) super().to_database(contact)
else: else:
@ -198,27 +198,27 @@ class AuthorizingOfficialForm(RegistrarForm):
# in either case, create a new contact and update it # in either case, create a new contact and update it
contact = Contact() contact = Contact()
super().to_database(contact) super().to_database(contact)
obj.authorizing_official = contact obj.senior_official = contact
obj.save() obj.save()
@classmethod @classmethod
def from_database(cls, obj): def from_database(cls, obj):
contact = getattr(obj, "authorizing_official", None) contact = getattr(obj, "senior_official", None)
return super().from_database(contact) return super().from_database(contact)
first_name = forms.CharField( first_name = forms.CharField(
label="First name / given name", label="First name / given name",
error_messages={"required": ("Enter the first name / given name of your authorizing official.")}, error_messages={"required": ("Enter the first name / given name of your senior official.")},
) )
last_name = forms.CharField( last_name = forms.CharField(
label="Last name / family name", label="Last name / family name",
error_messages={"required": ("Enter the last name / family name of your authorizing official.")}, error_messages={"required": ("Enter the last name / family name of your senior official.")},
) )
title = forms.CharField( title = forms.CharField(
label="Title or role in your organization", label="Title or role in your organization",
error_messages={ error_messages={
"required": ( "required": (
"Enter the title or role your authorizing official has in your" "Enter the title or role your senior official has in your"
" organization (e.g., Chief Information Officer)." " organization (e.g., Chief Information Officer)."
) )
}, },

View file

@ -390,7 +390,7 @@ class Command(BaseCommand):
fed_type = transition_domain.federal_type fed_type = transition_domain.federal_type
fed_agency = transition_domain.federal_agency fed_agency = transition_domain.federal_agency
# = AO Information = # # = SO Information = #
first_name = transition_domain.first_name first_name = transition_domain.first_name
middle_name = transition_domain.middle_name middle_name = transition_domain.middle_name
last_name = transition_domain.last_name last_name = transition_domain.last_name
@ -429,7 +429,7 @@ class Command(BaseCommand):
"domain": domain, "domain": domain,
"organization_name": transition_domain.organization_name, "organization_name": transition_domain.organization_name,
"creator": default_creator, "creator": default_creator,
"authorizing_official": contact, "senior_official": contact,
} }
if valid_org_type: if valid_org_type:

View file

@ -177,7 +177,7 @@ class LoadExtraTransitionDomain:
# STEP 3: Parse agency data # STEP 3: Parse agency data
updated_transition_domain = self.parse_agency_data(domain_name, transition_domain) updated_transition_domain = self.parse_agency_data(domain_name, transition_domain)
# STEP 4: Parse ao data # STEP 4: Parse so data
updated_transition_domain = self.parse_authority_data(domain_name, transition_domain) updated_transition_domain = self.parse_authority_data(domain_name, transition_domain)
# STEP 5: Parse creation and expiration data # STEP 5: Parse creation and expiration data
@ -326,7 +326,7 @@ class LoadExtraTransitionDomain:
) )
def parse_authority_data(self, domain_name, transition_domain) -> TransitionDomain: def parse_authority_data(self, domain_name, transition_domain) -> TransitionDomain:
"""Grabs authorizing_offical data from the parsed files and associates it """Grabs senior_offical data from the parsed files and associates it
with a transition_domain object, then returns that object.""" with a transition_domain object, then returns that object."""
if not isinstance(transition_domain, TransitionDomain): if not isinstance(transition_domain, TransitionDomain):
raise ValueError("Not a valid object, must be TransitionDomain") raise ValueError("Not a valid object, must be TransitionDomain")
@ -336,7 +336,7 @@ class LoadExtraTransitionDomain:
self.parse_logs.create_log_item( self.parse_logs.create_log_item(
EnumFilenames.AGENCY_ADHOC, EnumFilenames.AGENCY_ADHOC,
LogCode.ERROR, LogCode.ERROR,
f"Could not add authorizing_official on {domain_name}, no data exists.", f"Could not add senior_official on {domain_name}, no data exists.",
domain_name, domain_name,
not self.debug, not self.debug,
) )

View file

@ -0,0 +1,59 @@
# Generated by Django 4.2.10 on 2024-06-24 19:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0107_domainrequest_action_needed_reason_email"),
]
operations = [
migrations.RemoveField(
model_name="domaininformation",
name="authorizing_official",
),
migrations.RemoveField(
model_name="domainrequest",
name="authorizing_official",
),
migrations.AddField(
model_name="domaininformation",
name="senior_official",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="information_senior_official",
to="registrar.contact",
),
),
migrations.AddField(
model_name="domainrequest",
name="senior_official",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="senior_official",
to="registrar.contact",
),
),
migrations.AlterField(
model_name="domainrequest",
name="action_needed_reason",
field=models.TextField(
blank=True,
choices=[
("eligibility_unclear", "Unclear organization eligibility"),
("questionable_senior_official", "Questionable senior official"),
("already_has_domains", "Already has domains"),
("bad_name", "Doesnt meet naming requirements"),
("other", "Other (no auto-email sent)"),
],
null=True,
),
),
]

View file

@ -0,0 +1,85 @@
# Generated by Django 4.2.10 on 2024-07-02 16:59
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0108_domaininformation_authorizing_official_and_more"),
]
operations = [
migrations.AddField(
model_name="domaininformation",
name="sub_organization",
field=models.ForeignKey(
blank=True,
help_text="The suborganization that this domain is included under",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="information_sub_organization",
to="registrar.suborganization",
),
),
migrations.AddField(
model_name="domainrequest",
name="sub_organization",
field=models.ForeignKey(
blank=True,
help_text="The suborganization that this domain request is included under",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="request_sub_organization",
to="registrar.suborganization",
),
),
migrations.AlterField(
model_name="domaininformation",
name="portfolio",
field=models.ForeignKey(
blank=True,
help_text="Portfolio associated with this domain",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="information_portfolio",
to="registrar.portfolio",
),
),
migrations.AlterField(
model_name="domainrequest",
name="approved_domain",
field=models.OneToOneField(
blank=True,
help_text="Domain associated with this request; will be blank until request is approved",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="domain_request_approved_domain",
to="registrar.domain",
),
),
migrations.AlterField(
model_name="domainrequest",
name="portfolio",
field=models.ForeignKey(
blank=True,
help_text="Portfolio associated with this domain request",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="DomainRequest_portfolio",
to="registrar.portfolio",
),
),
migrations.AlterField(
model_name="domainrequest",
name="requested_domain",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="domain_request_requested_domain",
to="registrar.draftdomain",
),
),
]

View file

@ -63,10 +63,19 @@ class DomainInformation(TimeStampedModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
null=True, null=True,
blank=True, blank=True,
related_name="DomainRequest_portfolio", related_name="information_portfolio",
help_text="Portfolio associated with this domain", help_text="Portfolio associated with this domain",
) )
sub_organization = models.ForeignKey(
"registrar.Suborganization",
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="information_sub_organization",
help_text="The suborganization that this domain is included under",
)
domain_request = models.OneToOneField( domain_request = models.OneToOneField(
"registrar.DomainRequest", "registrar.DomainRequest",
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -171,11 +180,11 @@ class DomainInformation(TimeStampedModel):
blank=True, blank=True,
) )
authorizing_official = models.ForeignKey( senior_official = models.ForeignKey(
"registrar.Contact", "registrar.Contact",
null=True, null=True,
blank=True, blank=True,
related_name="information_authorizing_official", related_name="information_senior_official",
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
@ -361,6 +370,10 @@ class DomainInformation(TimeStampedModel):
# domain_request, if so short circuit the create # domain_request, if so short circuit the create
existing_domain_info = cls.objects.filter(domain_request__id=domain_request.id).first() existing_domain_info = cls.objects.filter(domain_request__id=domain_request.id).first()
if existing_domain_info: if existing_domain_info:
logger.info(
f"create_from_da() -> Shortcircuting create on {existing_domain_info}. "
"This record already exists. No values updated!"
)
return existing_domain_info return existing_domain_info
# Get the fields that exist on both DomainRequest and DomainInformation # Get the fields that exist on both DomainRequest and DomainInformation

View file

@ -266,7 +266,7 @@ class DomainRequest(TimeStampedModel):
"""Defines common action needed reasons for domain requests""" """Defines common action needed reasons for domain requests"""
ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility") ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
QUESTIONABLE_AUTHORIZING_OFFICIAL = ("questionable_authorizing_official", "Questionable authorizing official") QUESTIONABLE_SENIOR_OFFICIAL = ("questionable_senior_official", "Questionable senior official")
ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains") ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains")
BAD_NAME = ("bad_name", "Doesnt meet naming requirements") BAD_NAME = ("bad_name", "Doesnt meet naming requirements")
OTHER = ("other", "Other (no auto-email sent)") OTHER = ("other", "Other (no auto-email sent)")
@ -315,10 +315,19 @@ class DomainRequest(TimeStampedModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
null=True, null=True,
blank=True, blank=True,
related_name="DomainInformation_portfolio", related_name="DomainRequest_portfolio",
help_text="Portfolio associated with this domain request", help_text="Portfolio associated with this domain request",
) )
sub_organization = models.ForeignKey(
"registrar.Suborganization",
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="request_sub_organization",
help_text="The suborganization that this domain request is included under",
)
# This is the domain request user who created this domain request. The contact # This is the domain request user who created this domain request. The contact
# information that they gave is in the `submitter` field # information that they gave is in the `submitter` field
creator = models.ForeignKey( creator = models.ForeignKey(
@ -423,11 +432,11 @@ class DomainRequest(TimeStampedModel):
blank=True, blank=True,
) )
authorizing_official = models.ForeignKey( senior_official = models.ForeignKey(
"registrar.Contact", "registrar.Contact",
null=True, null=True,
blank=True, blank=True,
related_name="authorizing_official", related_name="senior_official",
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
@ -444,7 +453,7 @@ class DomainRequest(TimeStampedModel):
null=True, null=True,
blank=True, blank=True,
help_text="Domain associated with this request; will be blank until request is approved", help_text="Domain associated with this request; will be blank until request is approved",
related_name="domain_request", related_name="domain_request_approved_domain",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
@ -452,7 +461,7 @@ class DomainRequest(TimeStampedModel):
"DraftDomain", "DraftDomain",
null=True, null=True,
blank=True, blank=True,
related_name="domain_request", related_name="domain_request_requested_domain",
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
@ -1128,8 +1137,8 @@ class DomainRequest(TimeStampedModel):
and self.zipcode is None and self.zipcode is None
) )
def _is_authorizing_official_complete(self): def _is_senior_official_complete(self):
return self.authorizing_official is not None return self.senior_official is not None
def _is_requested_domain_complete(self): def _is_requested_domain_complete(self):
return self.requested_domain is not None return self.requested_domain is not None
@ -1191,10 +1200,10 @@ class DomainRequest(TimeStampedModel):
has_profile_feature_flag = flag_is_active(request, "profile_feature") has_profile_feature_flag = flag_is_active(request, "profile_feature")
return ( return (
self._is_organization_name_and_address_complete() self._is_organization_name_and_address_complete()
and self._is_authorizing_official_complete() and self._is_senior_official_complete()
and self._is_requested_domain_complete() and self._is_requested_domain_complete()
and self._is_purpose_complete() and self._is_purpose_complete()
# NOTE: This flag leaves submitter as empty (request wont submit) hence preset to True # NOTE: This flag leaves submitter as empty (request wont submit) hence set to True
and (self._is_submitter_complete() if not has_profile_feature_flag else True) and (self._is_submitter_complete() if not has_profile_feature_flag else True)
and self._is_other_contacts_complete() and self._is_other_contacts_complete()
and self._is_additional_details_complete() and self._is_additional_details_complete()

View file

@ -69,7 +69,7 @@
</h2> </h2>
<div class="usa-prose"> <div class="usa-prose">
<p> <p>
This will extend the expiration date by one year. This will extend the expiration date by one year from today.
{# Acts as a <br> #} {# Acts as a <br> #}
<div class="display-inline"></div> <div class="display-inline"></div>
This action cannot be undone. This action cannot be undone.
@ -78,7 +78,7 @@
Domain: <b>{{ original.name }}</b> Domain: <b>{{ original.name }}</b>
{# Acts as a <br> #} {# Acts as a <br> #}
<div class="display-inline"></div> <div class="display-inline"></div>
New expiration date: <b>{{ extended_expiration_date }}</b> Current expiration date: <b>{{ curr_exp_date }}</b>
{{test}} {{test}}
</p> </p>
</div> </div>

View file

@ -1,6 +1,6 @@
<p> <p>
Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request, Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request,
including other employees and authorizing officials. including other employees and senior officials.
Only contacts who have access to the registrar will have Only contacts who have access to the registrar will have
a corresponding record within the <a class="text-underline" href="{% url 'admin:registrar_user_changelist' %}">Users</a> table. a corresponding record within the <a class="text-underline" href="{% url 'admin:registrar_user_changelist' %}">Users</a> table.
</p> </p>

View file

@ -168,10 +168,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<label aria-label="Submitter contact details"></label> <label aria-label="Submitter contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %} {% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
</div> </div>
{% elif field.field.name == "authorizing_official" %} {% elif field.field.name == "senior_official" %}
<div class="flex-container"> <div class="flex-container">
<label aria-label="Authorizing official contact details"></label> <label aria-label="Senior official contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.authorizing_official no_title_top_padding=field.is_readonly %} {% include "django/admin/includes/contact_detail_list.html" with user=original_object.senior_official no_title_top_padding=field.is_readonly %}
</div> </div>
{% elif field.field.name == "other_contacts" and original_object.other_contacts.all %} {% elif field.field.name == "other_contacts" and original_object.other_contacts.all %}
{% with all_contacts=original_object.other_contacts.all %} {% with all_contacts=original_object.other_contacts.all %}

View file

@ -56,8 +56,8 @@
{% url 'domain-org-name-address' pk=domain.id as url %} {% url 'domain-org-name-address' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=domain.is_editable %} {% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=domain.is_editable %}
{% url 'domain-authorizing-official' pk=domain.id as url %} {% url 'domain-senior-official' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url editable=domain.is_editable %} {% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=domain.is_editable %}
{# Conditionally display profile #} {# Conditionally display profile #}
{% if not has_profile_feature_flag %} {% if not has_profile_feature_flag %}

View file

@ -1,37 +0,0 @@
{% extends 'domain_request_form.html' %}
{% load field_helpers url_helpers %}
{% block form_instructions %}
<h2 class="margin-bottom-05">
Who is the authorizing official for your organization?
</h2>
{% if not is_federal %}
<p>Your authorizing official is a person within your organization who can authorize your domain request. This person must be in a role of significant, executive responsibility within the organization.</p>
{% endif %}
<div class="ao_example">
{% include "includes/ao_example.html" %}
</div>
<p>We typically dont reach out to the authorizing official, but if contact is necessary, our practice is to coordinate with you, the requestor, first.</p>
{% endblock %}
{% block form_fields %}
<fieldset class="usa-fieldset">
<legend class="usa-sr-only">
Who is the authorizing official for your organization?
</legend>
{% input_with_errors forms.0.first_name %}
{% input_with_errors forms.0.last_name %}
{% input_with_errors forms.0.title %}
{% input_with_errors forms.0.email %}
</fieldset>
{% endblock %}

View file

@ -40,7 +40,7 @@
<p>We may need to suspend or terminate a domain registration for violations of these requirements. When we discover a violation, well make reasonable efforts to contact a registrant, including emails or phone calls to: <p>We may need to suspend or terminate a domain registration for violations of these requirements. When we discover a violation, well make reasonable efforts to contact a registrant, including emails or phone calls to:
<ul class="usa-list"> <ul class="usa-list">
<li>Domain contacts</li> <li>Domain contacts</li>
<li>The authorizing official</li> <li>The senior official</li>
<li>The government organization, a parent organization, or affiliated entities</li> <li>The government organization, a parent organization, or affiliated entities</li>
</ul> </ul>
</p> </p>

View file

@ -79,10 +79,10 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if step == Step.AUTHORIZING_OFFICIAL %} {% if step == Step.SENIOR_OFFICIAL %}
{% namespaced_url 'domain-request' step as domain_request_url %} {% namespaced_url 'domain-request' step as domain_request_url %}
{% if domain_request.authorizing_official is not None %} {% if domain_request.senior_official is not None %}
{% with title=form_titles|get_item:step value=domain_request.authorizing_official %} {% with title=form_titles|get_item:step value=domain_request.senior_official %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
{% endwith %} {% endwith %}
{% else %} {% else %}

View file

@ -0,0 +1,37 @@
{% extends 'domain_request_form.html' %}
{% load field_helpers url_helpers %}
{% block form_instructions %}
<h2 class="margin-bottom-05">
Who is the senior official for your organization?
</h2>
{% if not is_federal %}
<p>Your senior official is a person within your organization who can authorize your domain request. This person must be in a role of significant, executive responsibility within the organization.</p>
{% endif %}
<div class="so_example">
{% include "includes/so_example.html" %}
</div>
<p>We typically dont reach out to the senior official, but if contact is necessary, our practice is to coordinate with you, the requestor, first.</p>
{% endblock %}
{% block form_fields %}
<fieldset class="usa-fieldset">
<legend class="usa-sr-only">
Who is the senior official for your organization?
</legend>
{% input_with_errors forms.0.first_name %}
{% input_with_errors forms.0.last_name %}
{% input_with_errors forms.0.title %}
{% input_with_errors forms.0.email %}
</fieldset>
{% endblock %}

View file

@ -86,8 +86,8 @@
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %} {% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
{% endif %} {% endif %}
{% if DomainRequest.authorizing_official %} {% if DomainRequest.senior_official %}
{% include "includes/summary_item.html" with title='Authorizing official' value=DomainRequest.authorizing_official contact='true' heading_level=heading_level %} {% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
{% endif %} {% endif %}
{% if DomainRequest.current_websites.all %} {% if DomainRequest.current_websites.all %}

View file

@ -1,20 +1,20 @@
{% extends "domain_base.html" %} {% extends "domain_base.html" %}
{% load static field_helpers url_helpers %} {% load static field_helpers url_helpers %}
{% block title %}Authorizing official | {{ domain.name }} | {% endblock %} {% block title %}Senior official | {{ domain.name }} | {% endblock %}
{% block domain_content %} {% block domain_content %}
{# this is right after the messages block in the parent template #} {# this is right after the messages block in the parent template #}
{% include "includes/form_errors.html" with form=form %} {% include "includes/form_errors.html" with form=form %}
<h1>Authorizing official</h1> <h1>Senior official</h1>
<p>Your authorizing official is a person within your organization who can <p>Your senior official is a person within your organization who can
authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p> authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-a-senior-official-within-your-organization' %}">who can serve as a senior official</a>.</p>
{% if generic_org_type == "federal" or generic_org_type == "tribal" %} {% if generic_org_type == "federal" or generic_org_type == "tribal" %}
<p> <p>
The authorizing official for your organization cant be updated here. The senior official for your organization cant be updated here.
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>. To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p> </p>
{% else %} {% else %}
@ -27,10 +27,10 @@
{% if generic_org_type == "federal" or generic_org_type == "tribal" %} {% if generic_org_type == "federal" or generic_org_type == "tribal" %}
{# If all fields are disabled, add SR content #} {# If all fields are disabled, add SR content #}
<div class="usa-sr-only" aria-labelledby="id_first_name" id="sr-ao-first-name">{{ form.first_name.value }}</div> <div class="usa-sr-only" aria-labelledby="id_first_name" id="sr-so-first-name">{{ form.first_name.value }}</div>
<div class="usa-sr-only" aria-labelledby="id_last_name" id="sr-ao-last-name">{{ form.last_name.value }}</div> <div class="usa-sr-only" aria-labelledby="id_last_name" id="sr-so-last-name">{{ form.last_name.value }}</div>
<div class="usa-sr-only" aria-labelledby="id_title" id="sr-ao-title">{{ form.title.value }}</div> <div class="usa-sr-only" aria-labelledby="id_title" id="sr-so-title">{{ form.title.value }}</div>
<div class="usa-sr-only" aria-labelledby="id_email" id="sr-ao-email">{{ form.email.value }}</div> <div class="usa-sr-only" aria-labelledby="id_email" id="sr-so-email">{{ form.email.value }}</div>
{% endif %} {% endif %}
{% input_with_errors form.first_name %} {% input_with_errors form.first_name %}

View file

@ -65,11 +65,11 @@
</li> </li>
<li class="usa-sidenav__item"> <li class="usa-sidenav__item">
{% url 'domain-authorizing-official' pk=domain.id as url %} {% url 'domain-senior-official' pk=domain.id as url %}
<a href="{{ url }}" <a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %} {% if request.path == url %}class="usa-current"{% endif %}
> >
Authorizing official Senior official
</a> </a>
</li> </li>

View file

@ -8,7 +8,7 @@
<p> <p>
Domain managers can update all information related to a domain within the Domain managers can update all information related to a domain within the
.gov registrar, including contact details, authorizing official, security .gov registrar, including contact details, senior official, security
email, and DNS name servers. email, and DNS name servers.
</p> </p>

View file

@ -9,18 +9,18 @@ STATUS: Action needed
---------------------------------------------------------------- ----------------------------------------------------------------
AUTHORIZING OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS
We've reviewed your domain request, but we need more information about the authorizing official listed on the request: We've reviewed your domain request, but we need more information about the senior official listed on the request:
- {{ domain_request.authorizing_official.get_formatted_name }} - {{ domain_request.senior_official.get_formatted_name }}
- {{ domain_request.authorizing_official.title }} - {{ domain_request.senior_official.title }}
We expect an authorizing official to be someone in a role of significant, executive responsibility within the organization. Our guidelines are open-ended to accommodate the wide variety of government organizations that are eligible for .gov domains, but the person you listed does not meet our expectations for your type of organization. Read more about our guidelines for authorizing officials. <https://get.gov/domains/eligibility/> We expect a senior official to be someone in a role of significant, executive responsibility within the organization. Our guidelines are open-ended to accommodate the wide variety of government organizations that are eligible for .gov domains, but the person you listed does not meet our expectations for your type of organization. Read more about our guidelines for senior officials. <https://get.gov/domains/eligibility/>
ACTION NEEDED ACTION NEEDED
Reply to this email with a justification for naming {{ domain_request.authorizing_official.get_formatted_name }} as the authorizing official. If you have questions or comments, include those in your reply. Reply to this email with a justification for naming {{ domain_request.senior_official.get_formatted_name }} as the senior official. If you have questions or comments, include those in your reply.
Alternatively, you can log in to the registrar and enter a different authorizing official for this domain request. <https://manage.get.gov/> Once you submit your updated request, well resume the adjudication process. Alternatively, you can log in to the registrar and enter a different senior official for this domain request. <https://manage.get.gov/> Once you submit your updated request, well resume the adjudication process.
THANK YOU THANK YOU

View file

@ -27,8 +27,8 @@ Organization name and mailing address:
About your organization: About your organization:
{% spaceless %}{{ domain_request.about_your_organization }}{% endspaceless %} {% spaceless %}{{ domain_request.about_your_organization }}{% endspaceless %}
{% endif %} {% endif %}
Authorizing official: Senior official:
{% spaceless %}{% include "emails/includes/contact.txt" with contact=domain_request.authorizing_official %}{% endspaceless %} {% spaceless %}{% include "emails/includes/contact.txt" with contact=domain_request.senior_official %}{% endspaceless %}
{% if domain_request.current_websites.exists %}{# if block makes a newline #} {% if domain_request.current_websites.exists %}{# if block makes a newline #}
Current websites: {% for site in domain_request.current_websites.all %} Current websites: {% for site in domain_request.current_websites.all %}
{% spaceless %}{{ site.website }}{% endspaceless %} {% spaceless %}{{ site.website }}{% endspaceless %}

View file

@ -1,6 +1,6 @@
{% load static %} {% load static %}
<section class="section--outlined domain-requests"> <section class="section--outlined domain-requests" id="domain-requests">
<div class="grid-row"> <div class="grid-row">
{% if portfolio is None %} {% if portfolio is None %}
<div class="mobile:grid-col-12 desktop:grid-col-6"> <div class="mobile:grid-col-12 desktop:grid-col-6">
@ -11,10 +11,10 @@
<section aria-label="Domain requests search component" class="flex-6 margin-y-2"> <section aria-label="Domain requests search component" class="flex-6 margin-y-2">
<form class="usa-search usa-search--small" method="POST" role="search"> <form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %} {% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-button display-none" type="button"> <button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-search display-none" type="button">
Reset Reset
</button> </button>
<label class="usa-sr-only" for="domain-requests__search-field">Search</label> <label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
<input <input
class="usa-input" class="usa-input"
id="domain-requests__search-field" id="domain-requests__search-field"
@ -33,7 +33,7 @@
</section> </section>
</div> </div>
</div> </div>
<div class="domain-requests__table-wrapper display-none"> <div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table"> <table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
<caption class="sr-only">Your domain requests</caption> <caption class="sr-only">Your domain requests</caption>
<thead> <thead>
@ -58,7 +58,7 @@
<p>You haven't requested any domains.</p> <p>You haven't requested any domains.</p>
</div> </div>
<div class="domain-requests__no-search-results display-none"> <div class="domain-requests__no-search-results display-none">
<p>No results found for "<span class="domain-requests__search-term"></span>"</p> <p>No results found</p>
</div> </div>
</section> </section>
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination"> <nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">

View file

@ -1,6 +1,6 @@
{% load static %} {% load static %}
<section class="section--outlined domains"> <section class="section--outlined domains{% if portfolio is not None %} margin-top-0{% endif %}" id="domains">
<div class="grid-row"> <div class="grid-row">
{% if portfolio is None %} {% if portfolio is None %}
<div class="mobile:grid-col-12 desktop:grid-col-6"> <div class="mobile:grid-col-12 desktop:grid-col-6">
@ -11,10 +11,10 @@
<section aria-label="Domains search component" class="flex-6 margin-y-2"> <section aria-label="Domains search component" class="flex-6 margin-y-2">
<form class="usa-search usa-search--small" method="POST" role="search"> <form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %} {% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-button display-none" type="button"> <button class="usa-button usa-button--unstyled margin-right-2 domains__reset-search display-none" type="button">
Reset Reset
</button> </button>
<label class="usa-sr-only" for="domains__search-field">Search</label> <label class="usa-sr-only" for="domains__search-field">Search by domain name</label>
<input <input
class="usa-input" class="usa-input"
id="domains__search-field" id="domains__search-field"
@ -33,7 +33,102 @@
</section> </section>
</div> </div>
</div> </div>
<div class="domains__table-wrapper display-none"> {% if portfolio %}
<div class="display-flex flex-align-center margin-top-1">
<span class="margin-right-2 margin-top-neg-1 text-base-darker">Filter by</span>
<div class="usa-accordion usa-accordion--select margin-right-2">
<div class="usa-accordion__heading">
<button
type="button"
class="usa-button usa-button--small padding--8-8-9 usa-button--outline usa-button--filter usa-accordion__button"
aria-expanded="false"
aria-controls="filter-status"
>
<span class="domain__filter-indicator text-bold display-none"></span> Status
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</div>
<div id="filter-status" class="usa-accordion__content usa-prose shadow-1">
<h2>Status</h2>
<fieldset class="usa-fieldset margin-top-0">
<legend class="usa-legend">Select to apply <span class="sr-only">status</span> filter</legend>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-dns-needed"
type="checkbox"
name="filter-status"
value="unknown"
/>
<label class="usa-checkbox__label" for="filter-status-dns-needed"
>DNS Needed</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-ready"
type="checkbox"
name="filter-status"
value="ready"
/>
<label class="usa-checkbox__label" for="filter-status-ready"
>Ready</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-on-hold"
type="checkbox"
name="filter-status"
value="on hold"
/>
<label class="usa-checkbox__label" for="filter-status-on-hold"
>On hold</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-expired"
type="checkbox"
name="filter-status"
value="expired"
/>
<label class="usa-checkbox__label" for="filter-status-expired"
>Expired</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-deleted"
type="checkbox"
name="filter-status"
value="deleted"
/>
<label class="usa-checkbox__label" for="filter-status-deleted"
>Deleted</label
>
</div>
</fieldset>
</div>
</div>
<button
type="button"
class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domains__reset-filters display-none"
>
Clear filters
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#close"></use>
</svg>
</button>
</div>
{% endif %}
<div class="domains__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domains__table"> <table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domains__table">
<caption class="sr-only">Your registered domains</caption> <caption class="sr-only">Your registered domains</caption>
<thead> <thead>
@ -70,7 +165,7 @@
</p> </p>
</div> </div>
<div class="domains__no-search-results display-none"> <div class="domains__no-search-results display-none">
<p>No results found for "<span class="domains__search-term"></span>"</p> <p>No results found</p>
</div> </div>
</section> </section>
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination"> <nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">

View file

@ -3,6 +3,6 @@
{% load static %} {% load static %}
{% block portfolio_content %} {% block portfolio_content %}
<h1>Domains</h1> <h1 id="domains-header">Domains</h1>
{% include "includes/domains_table.html" with portfolio=portfolio %} {% include "includes/domains_table.html" with portfolio=portfolio %}
{% endblock %} {% endblock %}

View file

@ -3,7 +3,7 @@
{% load static %} {% load static %}
{% block portfolio_content %} {% block portfolio_content %}
<h1>Domain requests</h1> <h1 id="domain-requests-header">Domain requests</h1>
{% comment %} {% comment %}
IMPORTANT: IMPORTANT:

View file

@ -3,7 +3,7 @@
<div class="margin-bottom-4 tablet:margin-bottom-0"> <div class="margin-bottom-4 tablet:margin-bottom-0">
<nav aria-label=""> <nav aria-label="">
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2> <h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
<ul class="usa-sidenav"> <ul class="usa-sidenav usa-sidenav--portfolio">
<li class="usa-sidenav__item"> <li class="usa-sidenav__item">
{% url 'portfolio-domains' portfolio.id as url %} {% url 'portfolio-domains' portfolio.id as url %}
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}> <a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>

View file

@ -389,7 +389,7 @@ class AuditedAdminMockData:
zipcode: str = "10002", zipcode: str = "10002",
about_your_organization: str = "e-Government", about_your_organization: str = "e-Government",
anything_else: str = "There is more", anything_else: str = "There is more",
authorizing_official: Contact = self.dummy_contact(item_name, "authorizing_official"), senior_official: Contact = self.dummy_contact(item_name, "senior_official"),
submitter: Contact = self.dummy_contact(item_name, "submitter"), submitter: Contact = self.dummy_contact(item_name, "submitter"),
creator: User = self.dummy_user(item_name, "creator"), creator: User = self.dummy_user(item_name, "creator"),
} }
@ -407,7 +407,7 @@ class AuditedAdminMockData:
zipcode="10002", zipcode="10002",
about_your_organization="e-Government", about_your_organization="e-Government",
anything_else="There is more", anything_else="There is more",
authorizing_official=self.dummy_contact(item_name, "authorizing_official"), senior_official=self.dummy_contact(item_name, "senior_official"),
submitter=self.dummy_contact(item_name, "submitter"), submitter=self.dummy_contact(item_name, "submitter"),
creator=creator, creator=creator,
) )
@ -864,7 +864,7 @@ def completed_domain_request( # noqa
"""A completed domain request.""" """A completed domain request."""
if not user: if not user:
user = get_user_model().objects.create(username="username" + str(uuid.uuid4())[:8]) user = get_user_model().objects.create(username="username" + str(uuid.uuid4())[:8])
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -908,7 +908,7 @@ def completed_domain_request( # noqa
address_line2="address 2", address_line2="address 2",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
requested_domain=domain, requested_domain=domain,
submitter=submitter, submitter=submitter,
creator=user, creator=user,
@ -1551,8 +1551,6 @@ class MockEppLib(TestCase):
def mockInfoDomainCommands(self, _request, cleaned): def mockInfoDomainCommands(self, _request, cleaned):
request_name = getattr(_request, "name", None).lower() request_name = getattr(_request, "name", None).lower()
print(request_name)
# Define a dictionary to map request names to data and extension values # Define a dictionary to map request names to data and extension values
request_mappings = { request_mappings = {
"security.gov": (self.infoDomainNoContact, None), "security.gov": (self.infoDomainNoContact, None),

View file

@ -288,7 +288,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
self.assertContains(response, "(555) 555 5556") self.assertContains(response, "(555) 555 5556")
self.assertContains(response, "Testy2 Tester2") self.assertContains(response, "Testy2 Tester2")
# == Check for the authorizing_official == # # == Check for the senior_official == #
self.assertContains(response, "testy@town.com") self.assertContains(response, "testy@town.com")
self.assertContains(response, "Chief Tester") self.assertContains(response, "Chief Tester")
self.assertContains(response, "(555) 555 5555") self.assertContains(response, "(555) 555 5555")
@ -374,9 +374,9 @@ class TestDomainAdmin(MockEppLib, WebTest):
# Create a ready domain with a preset expiration date # Create a ready domain with a preset expiration date
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
# load expiration date into cache and registrar with below command
domain.registry_expiration_date
# Make sure the ex date is what we expect it to be # Make sure the ex date is what we expect it to be
domain_ex_date = Domain.objects.get(id=domain.id).expiration_date domain_ex_date = Domain.objects.get(id=domain.id).expiration_date
self.assertEqual(domain_ex_date, date(2023, 5, 25)) self.assertEqual(domain_ex_date, date(2023, 5, 25))
@ -400,7 +400,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name) self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date") 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 # Ensure the message we recieve is in line with what we expect
expected_message = "Successfully extended the expiration date." expected_message = "Successfully extended the expiration date."
@ -519,70 +518,10 @@ class TestDomainAdmin(MockEppLib, WebTest):
# Follow the response # Follow the response
response = response.follow() response = response.follow()
# This value is based off of the current year - the expiration date. # Assert that it is calling the function with the default extension length.
# We "freeze" time to 2024, so 2024 - 2023 will always result in an
# "extension" of 2, as that will be one year of extension from that date.
extension_length = 2
# Assert that it is calling the function with the right extension length.
# We only need to test the value that EPP sends, as we can assume the other # We only need to test the value that EPP sends, as we can assume the other
# test cases cover the "renew" function. # test cases cover the "renew" function.
renew_mock.assert_has_calls([call(length=extension_length)], any_order=False) renew_mock.assert_has_calls([call()], any_order=False)
# We should not make duplicate calls
self.assertEqual(renew_mock.call_count, 1)
# 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")
# Ensure the message we recieve is in line with what we expect
expected_message = "Successfully extended the expiration date."
expected_call = call(
# The WGSI request doesn't need to be tested
ANY,
messages.INFO,
expected_message,
extra_tags="",
fail_silently=False,
)
mock_add_message.assert_has_calls([expected_call], 1)
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2023, 1, 1))
def test_extend_expiration_date_button_date_matches_epp(self, mock_date_today):
"""
Tests if extend_expiration_date button sends the right epp command
when the current year matches the expiration date
"""
# Create a ready domain with a preset expiration date
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
# Make sure that the page is loading as expected
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date")
# Grab the form to submit
form = response.forms["domain_form"]
with patch("django.contrib.messages.add_message") as mock_add_message:
with patch("registrar.models.Domain.renew_domain") as renew_mock:
# Submit the form
response = form.submit("_extend_expiration_date")
# Follow the response
response = response.follow()
extension_length = 1
# Assert that it is calling the function with the right extension length.
# We only need to test the value that EPP sends, as we can assume the other
# test cases cover the "renew" function.
renew_mock.assert_has_calls([call(length=extension_length)], any_order=False)
# We should not make duplicate calls # We should not make duplicate calls
self.assertEqual(renew_mock.call_count, 1) self.assertEqual(renew_mock.call_count, 1)
@ -1525,11 +1464,11 @@ class TestDomainRequestAdmin(MockEppLib):
) )
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test the email sent out for questionable_ao # Test the email sent out for questionable_so
questionable_ao = DomainRequest.ActionNeededReasons.QUESTIONABLE_AUTHORIZING_OFFICIAL questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_ao) self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
self.assert_email_is_accurate( self.assert_email_is_accurate(
"AUTHORIZING OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL "SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL
) )
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
@ -2175,16 +2114,15 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields) self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
self.assertContains(response, "Testy2 Tester2") self.assertContains(response, "Testy2 Tester2")
# == Check for the authorizing_official == # # == Check for the senior_official == #
self.assertContains(response, "testy@town.com", count=2) self.assertContains(response, "testy@town.com", count=2)
expected_ao_fields = [ expected_so_fields = [
# Field, expected value # Field, expected value
("phone", "(555) 555 5555"), ("phone", "(555) 555 5555"),
] ]
self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields)
self.assertContains(response, "Chief Tester")
self.assertContains(response, "Testy Tester") self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields)
self.assertContains(response, "Chief Tester")
# == Test the other_employees field == # # == Test the other_employees field == #
self.assertContains(response, "testy2@town.com") self.assertContains(response, "testy2@town.com")
@ -2313,6 +2251,7 @@ class TestDomainRequestAdmin(MockEppLib):
"action_needed_reason_email", "action_needed_reason_email",
"federal_agency", "federal_agency",
"portfolio", "portfolio",
"sub_organization",
"creator", "creator",
"investigator", "investigator",
"generic_org_type", "generic_org_type",
@ -2330,7 +2269,7 @@ class TestDomainRequestAdmin(MockEppLib):
"zipcode", "zipcode",
"urbanization", "urbanization",
"about_your_organization", "about_your_organization",
"authorizing_official", "senior_official",
"approved_domain", "approved_domain",
"requested_domain", "requested_domain",
"submitter", "submitter",
@ -3224,14 +3163,14 @@ class TestDomainInformationAdmin(TestCase):
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields) self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
self.assertContains(response, "Testy2 Tester2") self.assertContains(response, "Testy2 Tester2")
# == Check for the authorizing_official == # # == Check for the senior_official == #
self.assertContains(response, "testy@town.com", count=2) self.assertContains(response, "testy@town.com", count=2)
expected_ao_fields = [ expected_so_fields = [
# Field, expected value # Field, expected value
("title", "Chief Tester"), ("title", "Chief Tester"),
("phone", "(555) 555 5555"), ("phone", "(555) 555 5555"),
] ]
self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields) self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields)
self.assertContains(response, "Testy Tester", count=10) self.assertContains(response, "Testy Tester", count=10)
@ -3625,7 +3564,7 @@ class TestMyUserAdmin(MockDb):
) )
}, },
), ),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
("Permissions", {"fields": ("is_active", "groups")}), ("Permissions", {"fields": ("is_active", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
) )
@ -3779,7 +3718,7 @@ class AuditedAdminTest(TestCase):
def test_alphabetically_sorted_fk_fields_domain_request(self): def test_alphabetically_sorted_fk_fields_domain_request(self):
with less_console_noise(): with less_console_noise():
tested_fields = [ tested_fields = [
DomainRequest.authorizing_official.field, DomainRequest.senior_official.field,
DomainRequest.submitter.field, DomainRequest.submitter.field,
# DomainRequest.investigator.field, # DomainRequest.investigator.field,
DomainRequest.creator.field, DomainRequest.creator.field,
@ -3837,7 +3776,7 @@ class AuditedAdminTest(TestCase):
def test_alphabetically_sorted_fk_fields_domain_information(self): def test_alphabetically_sorted_fk_fields_domain_information(self):
with less_console_noise(): with less_console_noise():
tested_fields = [ tested_fields = [
DomainInformation.authorizing_official.field, DomainInformation.senior_official.field,
DomainInformation.submitter.field, DomainInformation.submitter.field,
# DomainInformation.creator.field, # DomainInformation.creator.field,
(DomainInformation.domain.field, ["name"]), (DomainInformation.domain.field, ["name"]),
@ -4116,9 +4055,7 @@ class TestContactAdmin(TestCase):
readonly_fields = self.admin.get_readonly_fields(request) readonly_fields = self.admin.get_readonly_fields(request)
expected_fields = [ expected_fields = ["user", "email"]
"user",
]
self.assertEqual(readonly_fields, expected_fields) self.assertEqual(readonly_fields, expected_fields)

View file

@ -59,7 +59,7 @@ class TestEmails(TestCase):
self.assertIn("Type of organization:", body) self.assertIn("Type of organization:", body)
self.assertIn("Federal", body) self.assertIn("Federal", body)
self.assertIn("Authorizing official:", body) self.assertIn("Senior official:", body)
self.assertIn("Testy Tester", body) self.assertIn("Testy Tester", body)
self.assertIn(".gov domain:", body) self.assertIn(".gov domain:", body)
self.assertIn("city.gov", body) self.assertIn("city.gov", body)
@ -177,7 +177,7 @@ class TestEmails(TestCase):
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertNotIn("About your organization:", body) self.assertNotIn("About your organization:", body)
# spacing should be right between adjacent elements # spacing should be right between adjacent elements
self.assertRegex(body, r"10002\n\nAuthorizing official:") self.assertRegex(body, r"10002\n\nSenior official:")
@boto3_mocking.patching @boto3_mocking.patching
def test_submission_confirmation_anything_else_spacing(self): def test_submission_confirmation_anything_else_spacing(self):

View file

@ -8,7 +8,7 @@ from registrar.forms.domain_request_wizard import (
AlternativeDomainForm, AlternativeDomainForm,
CurrentSitesForm, CurrentSitesForm,
DotGovDomainForm, DotGovDomainForm,
AuthorizingOfficialForm, SeniorOfficialForm,
OrganizationContactForm, OrganizationContactForm,
YourContactForm, YourContactForm,
OtherContactsForm, OtherContactsForm,
@ -217,9 +217,9 @@ class TestFormValidation(MockEppLib):
["Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens)."], ["Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens)."],
) )
def test_authorizing_official_email_invalid(self): def test_senior_official_email_invalid(self):
"""must be a valid email address.""" """must be a valid email address."""
form = AuthorizingOfficialForm(data={"email": "boss@boss"}) form = SeniorOfficialForm(data={"email": "boss@boss"})
self.assertEqual( self.assertEqual(
form.errors["email"], form.errors["email"],
["Enter an email address in the required format, like name@example.com."], ["Enter an email address in the required format, like name@example.com."],

View file

@ -151,7 +151,7 @@ class TestDomainRequest(TestCase):
address_line2="APT 1A", address_line2="APT 1A",
state_territory="CA", state_territory="CA",
zipcode="12345-6789", zipcode="12345-6789",
authorizing_official=contact, senior_official=contact,
requested_domain=domain, requested_domain=domain,
submitter=contact, submitter=contact,
purpose="Igorville rules!", purpose="Igorville rules!",
@ -179,7 +179,7 @@ class TestDomainRequest(TestCase):
address_line2="APT 1A", address_line2="APT 1A",
state_territory="CA", state_territory="CA",
zipcode="12345-6789", zipcode="12345-6789",
authorizing_official=contact, senior_official=contact,
submitter=contact, submitter=contact,
purpose="Igorville rules!", purpose="Igorville rules!",
anything_else="All of Igorville loves the dotgov program.", anything_else="All of Igorville loves the dotgov program.",
@ -1233,8 +1233,8 @@ class TestContact(TestCase):
) )
self.contact, _ = Contact.objects.get_or_create(user=self.user) self.contact, _ = Contact.objects.get_or_create(user=self.user)
self.contact_as_ao, _ = Contact.objects.get_or_create(email="newguy@igorville.gov") self.contact_as_so, _ = Contact.objects.get_or_create(email="newguy@igorville.gov")
self.domain_request = DomainRequest.objects.create(creator=self.user, authorizing_official=self.contact_as_ao) self.domain_request = DomainRequest.objects.create(creator=self.user, senior_official=self.contact_as_so)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
@ -1327,10 +1327,10 @@ class TestContact(TestCase):
"""Test the Contact model method, has_more_than_one_join""" """Test the Contact model method, has_more_than_one_join"""
# test for a contact which has one user defined # test for a contact which has one user defined
self.assertFalse(self.contact.has_more_than_one_join("user")) self.assertFalse(self.contact.has_more_than_one_join("user"))
self.assertTrue(self.contact.has_more_than_one_join("authorizing_official")) self.assertTrue(self.contact.has_more_than_one_join("senior_official"))
# test for a contact which is assigned as an authorizing official on a domain request # test for a contact which is assigned as a senior official on a domain request
self.assertFalse(self.contact_as_ao.has_more_than_one_join("authorizing_official")) self.assertFalse(self.contact_as_so.has_more_than_one_join("senior_official"))
self.assertTrue(self.contact_as_ao.has_more_than_one_join("submitted_domain_requests")) self.assertTrue(self.contact_as_so.has_more_than_one_join("submitted_domain_requests"))
def test_has_contact_info(self): def test_has_contact_info(self):
"""Test that has_contact_info properly returns""" """Test that has_contact_info properly returns"""
@ -1660,7 +1660,7 @@ class TestDomainRequestIncomplete(TestCase):
self.user = get_user_model().objects.create( self.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email username=username, first_name=first_name, last_name=last_name, email=email
) )
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Meowy", first_name="Meowy",
last_name="Meoward", last_name="Meoward",
title="Chief Cat", title="Chief Cat",
@ -1695,7 +1695,7 @@ class TestDomainRequestIncomplete(TestCase):
address_line1="address 1", address_line1="address 1",
state_territory="CA", state_territory="CA",
zipcode="94044", zipcode="94044",
authorizing_official=ao, senior_official=so,
requested_domain=draft_domain, requested_domain=draft_domain,
purpose="Some purpose", purpose="Some purpose",
submitter=you, submitter=you,
@ -1793,11 +1793,11 @@ class TestDomainRequestIncomplete(TestCase):
self.domain_request.save() self.domain_request.save()
self.assertTrue(self.domain_request._is_organization_name_and_address_complete()) self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
def test_is_authorizing_official_complete(self): def test_is_senior_official_complete(self):
self.assertTrue(self.domain_request._is_authorizing_official_complete()) self.assertTrue(self.domain_request._is_senior_official_complete())
self.domain_request.authorizing_official = None self.domain_request.senior_official = None
self.domain_request.save() self.domain_request.save()
self.assertFalse(self.domain_request._is_authorizing_official_complete()) self.assertFalse(self.domain_request._is_senior_official_complete())
def test_is_requested_domain_complete(self): def test_is_requested_domain_complete(self):
self.assertTrue(self.domain_request._is_requested_domain_complete()) self.assertTrue(self.domain_request._is_requested_domain_complete())

View file

@ -232,8 +232,8 @@ class ExportDataTest(MockDb, MockEppLib):
"Organization name", "Organization name",
"City", "City",
"State", "State",
"AO", "SO",
"AO email", "SO email",
"Security contact email", "Security contact email",
"Status", "Status",
"Expiration date", "Expiration date",
@ -265,8 +265,8 @@ class ExportDataTest(MockDb, MockEppLib):
# We expect READY domains, # We expect READY domains,
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,AO," "Domain name,Domain type,Agency,Organization name,City,State,SO,"
"AO email,Security contact email,Status,Expiration date, First ready on\n" "SO email,Security contact email,Status,Expiration date, First ready on\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2024-04-03\n" "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2024-04-03\n"
"adomain2.gov,Interstate,(blank),Dns needed,(blank),(blank)\n" "adomain2.gov,Interstate,(blank),Dns needed,(blank),(blank)\n"
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2024-04-02\n" "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2024-04-02\n"
@ -299,8 +299,8 @@ class ExportDataTest(MockDb, MockEppLib):
"Organization name", "Organization name",
"City", "City",
"State", "State",
"AO", "SO",
"AO email", "SO email",
"Submitter", "Submitter",
"Submitter title", "Submitter title",
"Submitter email", "Submitter email",
@ -332,8 +332,8 @@ class ExportDataTest(MockDb, MockEppLib):
# We expect READY domains, # We expect READY domains,
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,AO," "Domain name,Domain type,Agency,Organization name,City,State,SO,"
"AO email,Submitter,Submitter title,Submitter email,Submitter phone," "SO email,Submitter,Submitter title,Submitter email,Submitter phone,"
"Security contact email,Status\n" "Security contact email,Status\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
"adomain2.gov,Interstate,Dns needed\n" "adomain2.gov,Interstate,Dns needed\n"
@ -522,8 +522,8 @@ class ExportDataTest(MockDb, MockEppLib):
"Organization name", "Organization name",
"City", "City",
"State", "State",
"AO", "SO",
"AO email", "SO email",
"Security contact email", "Security contact email",
] ]
sort_fields = ["domain__name"] sort_fields = ["domain__name"]
@ -553,7 +553,7 @@ class ExportDataTest(MockDb, MockEppLib):
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
"Domain name,Status,Expiration date,Domain type,Agency," "Domain name,Status,Expiration date,Domain type,Agency,"
"Organization name,City,State,AO,AO email," "Organization name,City,State,SO,SO email,"
"Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status," "Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n" "Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
"adomain10.gov,Ready,(blank),Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n" "adomain10.gov,Ready,(blank),Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
@ -717,10 +717,10 @@ class ExportDataTest(MockDb, MockEppLib):
additional_values = [ additional_values = [
"requested_domain__name", "requested_domain__name",
"federal_agency__agency", "federal_agency__agency",
"authorizing_official__first_name", "senior_official__first_name",
"authorizing_official__last_name", "senior_official__last_name",
"authorizing_official__email", "senior_official__email",
"authorizing_official__title", "senior_official__title",
"creator__first_name", "creator__first_name",
"creator__last_name", "creator__last_name",
"creator__email", "creator__email",
@ -735,15 +735,13 @@ class ExportDataTest(MockDb, MockEppLib):
csv_file.seek(0) csv_file.seek(0)
# Read the content into a variable # Read the content into a variable
csv_content = csv_file.read() csv_content = csv_file.read()
print(csv_content)
self.maxDiff = None
expected_content = ( expected_content = (
# Header # Header
"Domain request,Submitted at,Status,Domain type,Federal type," "Domain request,Submitted at,Status,Domain type,Federal type,"
"Federal agency,Organization name,Election office,City,State/territory," "Federal agency,Organization name,Election office,City,State/territory,"
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count," "Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
"Creator active requests count,Alternative domains,AO first name,AO last name,AO email," "Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
"AO title/role,Request purpose,Request additional details,Other contacts," "SO title/role,Request purpose,Request additional details,Other contacts,"
"CISA regional representative,Current websites,Investigator\n" "CISA regional representative,Current websites,Investigator\n"
# Content # Content
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com," "city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"

View file

@ -398,7 +398,7 @@ class TestOrganizationMigration(TestCase):
federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce") federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce")
expected_creator = User.objects.filter(username="System").get() expected_creator = User.objects.filter(username="System").get()
expected_ao = Contact.objects.filter( expected_so = Contact.objects.filter(
first_name="Seline", middle_name="testmiddle2", last_name="Tower" first_name="Seline", middle_name="testmiddle2", last_name="Tower"
).get() ).get()
expected_domain_information = DomainInformation( expected_domain_information = DomainInformation(
@ -411,7 +411,7 @@ class TestOrganizationMigration(TestCase):
city="Columbus", city="Columbus",
state_territory="Oh", state_territory="Oh",
zipcode="43268", zipcode="43268",
authorizing_official=expected_ao, senior_official=expected_so,
domain=_domain, domain=_domain,
) )
# Given that these are different objects, this needs to be set # Given that these are different objects, this needs to be set
@ -454,7 +454,7 @@ class TestOrganizationMigration(TestCase):
federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce") federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce")
expected_creator = User.objects.filter(username="System").get() expected_creator = User.objects.filter(username="System").get()
expected_ao = Contact.objects.filter( expected_so = Contact.objects.filter(
first_name="Seline", middle_name="testmiddle2", last_name="Tower" first_name="Seline", middle_name="testmiddle2", last_name="Tower"
).get() ).get()
expected_domain_information = DomainInformation( expected_domain_information = DomainInformation(
@ -467,7 +467,7 @@ class TestOrganizationMigration(TestCase):
city="Olympus", city="Olympus",
state_territory="MA", state_territory="MA",
zipcode="12345", zipcode="12345",
authorizing_official=expected_ao, senior_official=expected_so,
domain=_domain, domain=_domain,
) )
# Given that these are different objects, this needs to be set # Given that these are different objects, this needs to be set

View file

@ -381,7 +381,7 @@ class HomeTests(TestWithUser):
creator=self.user, creator=self.user,
requested_domain=site, requested_domain=site,
status=DomainRequest.DomainRequestStatus.WITHDRAWN, status=DomainRequest.DomainRequestStatus.WITHDRAWN,
authorizing_official=contact, senior_official=contact,
submitter=contact_user, submitter=contact_user,
) )
domain_request.other_contacts.set([contact_2]) domain_request.other_contacts.set([contact_2])
@ -392,7 +392,7 @@ class HomeTests(TestWithUser):
creator=self.user, creator=self.user,
requested_domain=site_2, requested_domain=site_2,
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
authorizing_official=contact_2, senior_official=contact_2,
submitter=contact_shared, submitter=contact_shared,
) )
domain_request_2.other_contacts.set([contact_shared]) domain_request_2.other_contacts.set([contact_shared])
@ -453,7 +453,7 @@ class HomeTests(TestWithUser):
creator=self.user, creator=self.user,
requested_domain=site, requested_domain=site,
status=DomainRequest.DomainRequestStatus.WITHDRAWN, status=DomainRequest.DomainRequestStatus.WITHDRAWN,
authorizing_official=contact, senior_official=contact,
submitter=contact_user, submitter=contact_user,
) )
domain_request.other_contacts.set([contact_2]) domain_request.other_contacts.set([contact_2])
@ -464,7 +464,7 @@ class HomeTests(TestWithUser):
creator=self.user, creator=self.user,
requested_domain=site_2, requested_domain=site_2,
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
authorizing_official=contact_2, senior_official=contact_2,
submitter=contact_shared, submitter=contact_shared,
) )
domain_request_2.other_contacts.set([contact_shared]) domain_request_2.other_contacts.set([contact_shared])
@ -871,7 +871,7 @@ class UserProfileTests(TestWithUser, WebTest):
creator=self.user, creator=self.user,
requested_domain=site, requested_domain=site,
status=DomainRequest.DomainRequestStatus.SUBMITTED, status=DomainRequest.DomainRequestStatus.SUBMITTED,
authorizing_official=contact_user, senior_official=contact_user,
submitter=contact_user, submitter=contact_user,
) )
with override_flag("profile_feature", active=True): with override_flag("profile_feature", active=True):
@ -890,7 +890,7 @@ class UserProfileTests(TestWithUser, WebTest):
creator=self.user, creator=self.user,
requested_domain=site, requested_domain=site,
status=DomainRequest.DomainRequestStatus.SUBMITTED, status=DomainRequest.DomainRequestStatus.SUBMITTED,
authorizing_official=contact_user, senior_official=contact_user,
submitter=contact_user, submitter=contact_user,
) )
with override_flag("profile_feature", active=False): with override_flag("profile_feature", active=False):
@ -965,7 +965,7 @@ class PortfoliosTests(TestWithUser, WebTest):
# Assert that we're on the right page # Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name) self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertContains(portfolio_page, "<h1>Domains</h1>") self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
@less_console_noise_decorator @less_console_noise_decorator
def test_no_redirect_when_org_flag_false(self): def test_no_redirect_when_org_flag_false(self):

View file

@ -150,7 +150,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-users-add", "domain-users-add",
"domain-dns-nameservers", "domain-dns-nameservers",
"domain-org-name-address", "domain-org-name-address",
"domain-authorizing-official", "domain-senior-official",
"domain-your-contact-information", "domain-your-contact-information",
"domain-security-email", "domain-security-email",
]: ]:
@ -169,7 +169,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-users-add", "domain-users-add",
"domain-dns-nameservers", "domain-dns-nameservers",
"domain-org-name-address", "domain-org-name-address",
"domain-authorizing-official", "domain-senior-official",
"domain-your-contact-information", "domain-your-contact-information",
"domain-security-email", "domain-security-email",
]: ]:
@ -190,7 +190,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-dns-dnssec", "domain-dns-dnssec",
"domain-dns-dnssec-dsdata", "domain-dns-dnssec-dsdata",
"domain-org-name-address", "domain-org-name-address",
"domain-authorizing-official", "domain-senior-official",
"domain-your-contact-information", "domain-your-contact-information",
"domain-security-email", "domain-security-email",
]: ]:
@ -1082,44 +1082,43 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
) )
class TestDomainAuthorizingOfficial(TestDomainOverview): class TestDomainSeniorOfficial(TestDomainOverview):
def test_domain_authorizing_official(self): def test_domain_senior_official(self):
"""Can load domain's authorizing official page.""" """Can load domain's senior official page."""
page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) page = self.client.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
# once on the sidebar, once in the title self.assertContains(page, "Senior official", count=13)
self.assertContains(page, "Authorizing official", count=3)
def test_domain_authorizing_official_content(self): def test_domain_senior_official_content(self):
"""Authorizing official information appears on the page.""" """Senior official information appears on the page."""
self.domain_information.authorizing_official = Contact(first_name="Testy") self.domain_information.senior_official = Contact(first_name="Testy")
self.domain_information.authorizing_official.save() self.domain_information.senior_official.save()
self.domain_information.save() self.domain_information.save()
page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Testy") self.assertContains(page, "Testy")
def test_domain_edit_authorizing_official_in_place(self): def test_domain_edit_senior_official_in_place(self):
"""When editing an authorizing official for domain information and AO is not """When editing a senior official for domain information and SO is not
joined to any other objects""" joined to any other objects"""
self.domain_information.authorizing_official = Contact( self.domain_information.senior_official = Contact(
first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov"
) )
self.domain_information.authorizing_official.save() self.domain_information.senior_official.save()
self.domain_information.save() self.domain_information.save()
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
self.assertEqual(ao_form["first_name"].value, "Testy") self.assertEqual(so_form["first_name"].value, "Testy")
ao_form["first_name"] = "Testy2" so_form["first_name"] = "Testy2"
# ao_pk is the initial pk of the authorizing official. set it before update # so_pk is the initial pk of the senior official. set it before update
# to be able to verify after update that the same contact object is in place # to be able to verify after update that the same contact object is in place
ao_pk = self.domain_information.authorizing_official.id so_pk = self.domain_information.senior_official.id
ao_form.submit() so_form.submit()
# refresh domain information # refresh domain information
self.domain_information.refresh_from_db() self.domain_information.refresh_from_db()
self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) self.assertEqual("Testy2", self.domain_information.senior_official.first_name)
self.assertEqual(ao_pk, self.domain_information.authorizing_official.id) self.assertEqual(so_pk, self.domain_information.senior_official.id)
def assert_all_form_fields_have_expected_values(self, form, test_cases, test_for_disabled=False): def assert_all_form_fields_have_expected_values(self, form, test_cases, test_for_disabled=False):
""" """
@ -1147,26 +1146,26 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
# Test for disabled on each field # Test for disabled on each field
self.assertTrue("disabled" in form[field_name].attrs) self.assertTrue("disabled" in form[field_name].attrs)
def test_domain_edit_authorizing_official_federal(self): def test_domain_edit_senior_official_federal(self):
"""Tests that no edit can occur when the underlying domain is federal""" """Tests that no edit can occur when the underlying domain is federal"""
# Set the org type to federal # Set the org type to federal
self.domain_information.generic_org_type = DomainInformation.OrganizationChoices.FEDERAL self.domain_information.generic_org_type = DomainInformation.OrganizationChoices.FEDERAL
self.domain_information.save() self.domain_information.save()
# Add an AO. We can do this at the model level, just not the form level. # Add an SO. We can do this at the model level, just not the form level.
self.domain_information.authorizing_official = Contact( self.domain_information.senior_official = Contact(
first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov" first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov"
) )
self.domain_information.authorizing_official.save() self.domain_information.senior_official.save()
self.domain_information.save() self.domain_information.save()
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Test if the form is populating data correctly # Test if the form is populating data correctly
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
test_cases = [ test_cases = [
("first_name", "Apple"), ("first_name", "Apple"),
@ -1174,16 +1173,16 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
("title", "CIO"), ("title", "CIO"),
("email", "nobody@igorville.gov"), ("email", "nobody@igorville.gov"),
] ]
self.assert_all_form_fields_have_expected_values(ao_form, test_cases, test_for_disabled=True) self.assert_all_form_fields_have_expected_values(so_form, test_cases, test_for_disabled=True)
# Attempt to change data on each field. Because this domain is federal, # Attempt to change data on each field. Because this domain is federal,
# this should not succeed. # this should not succeed.
ao_form["first_name"] = "Orange" so_form["first_name"] = "Orange"
ao_form["last_name"] = "Smoothie" so_form["last_name"] = "Smoothie"
ao_form["title"] = "Cat" so_form["title"] = "Cat"
ao_form["email"] = "somebody@igorville.gov" so_form["email"] = "somebody@igorville.gov"
submission = ao_form.submit() submission = so_form.submit()
# A 302 indicates this page underwent a redirect. # A 302 indicates this page underwent a redirect.
self.assertEqual(submission.status_code, 302) self.assertEqual(submission.status_code, 302)
@ -1198,31 +1197,31 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
self.domain_information.refresh_from_db() self.domain_information.refresh_from_db()
# All values should be unchanged. These are defined manually for code clarity. # All values should be unchanged. These are defined manually for code clarity.
self.assertEqual("Apple", self.domain_information.authorizing_official.first_name) self.assertEqual("Apple", self.domain_information.senior_official.first_name)
self.assertEqual("Tester", self.domain_information.authorizing_official.last_name) self.assertEqual("Tester", self.domain_information.senior_official.last_name)
self.assertEqual("CIO", self.domain_information.authorizing_official.title) self.assertEqual("CIO", self.domain_information.senior_official.title)
self.assertEqual("nobody@igorville.gov", self.domain_information.authorizing_official.email) self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email)
def test_domain_edit_authorizing_official_tribal(self): def test_domain_edit_senior_official_tribal(self):
"""Tests that no edit can occur when the underlying domain is tribal""" """Tests that no edit can occur when the underlying domain is tribal"""
# Set the org type to federal # Set the org type to federal
self.domain_information.generic_org_type = DomainInformation.OrganizationChoices.TRIBAL self.domain_information.generic_org_type = DomainInformation.OrganizationChoices.TRIBAL
self.domain_information.save() self.domain_information.save()
# Add an AO. We can do this at the model level, just not the form level. # Add an SO. We can do this at the model level, just not the form level.
self.domain_information.authorizing_official = Contact( self.domain_information.senior_official = Contact(
first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov" first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov"
) )
self.domain_information.authorizing_official.save() self.domain_information.senior_official.save()
self.domain_information.save() self.domain_information.save()
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Test if the form is populating data correctly # Test if the form is populating data correctly
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
test_cases = [ test_cases = [
("first_name", "Apple"), ("first_name", "Apple"),
@ -1230,16 +1229,16 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
("title", "CIO"), ("title", "CIO"),
("email", "nobody@igorville.gov"), ("email", "nobody@igorville.gov"),
] ]
self.assert_all_form_fields_have_expected_values(ao_form, test_cases, test_for_disabled=True) self.assert_all_form_fields_have_expected_values(so_form, test_cases, test_for_disabled=True)
# Attempt to change data on each field. Because this domain is federal, # Attempt to change data on each field. Because this domain is federal,
# this should not succeed. # this should not succeed.
ao_form["first_name"] = "Orange" so_form["first_name"] = "Orange"
ao_form["last_name"] = "Smoothie" so_form["last_name"] = "Smoothie"
ao_form["title"] = "Cat" so_form["title"] = "Cat"
ao_form["email"] = "somebody@igorville.gov" so_form["email"] = "somebody@igorville.gov"
submission = ao_form.submit() submission = so_form.submit()
# A 302 indicates this page underwent a redirect. # A 302 indicates this page underwent a redirect.
self.assertEqual(submission.status_code, 302) self.assertEqual(submission.status_code, 302)
@ -1254,45 +1253,45 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
self.domain_information.refresh_from_db() self.domain_information.refresh_from_db()
# All values should be unchanged. These are defined manually for code clarity. # All values should be unchanged. These are defined manually for code clarity.
self.assertEqual("Apple", self.domain_information.authorizing_official.first_name) self.assertEqual("Apple", self.domain_information.senior_official.first_name)
self.assertEqual("Tester", self.domain_information.authorizing_official.last_name) self.assertEqual("Tester", self.domain_information.senior_official.last_name)
self.assertEqual("CIO", self.domain_information.authorizing_official.title) self.assertEqual("CIO", self.domain_information.senior_official.title)
self.assertEqual("nobody@igorville.gov", self.domain_information.authorizing_official.email) self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email)
def test_domain_edit_authorizing_official_creates_new(self): def test_domain_edit_senior_official_creates_new(self):
"""When editing an authorizing official for domain information and AO IS """When editing a senior official for domain information and SO IS
joined to another object""" joined to another object"""
# set AO and Other Contact to the same Contact object # set SO and Other Contact to the same Contact object
self.domain_information.authorizing_official = Contact( self.domain_information.senior_official = Contact(
first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov"
) )
self.domain_information.authorizing_official.save() self.domain_information.senior_official.save()
self.domain_information.save() self.domain_information.save()
self.domain_information.other_contacts.add(self.domain_information.authorizing_official) self.domain_information.other_contacts.add(self.domain_information.senior_official)
self.domain_information.save() self.domain_information.save()
# load the Authorizing Official in the web form # load the Senior Official in the web form
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) so_page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
# verify the first name is "Testy" and then change it to "Testy2" # verify the first name is "Testy" and then change it to "Testy2"
self.assertEqual(ao_form["first_name"].value, "Testy") self.assertEqual(so_form["first_name"].value, "Testy")
ao_form["first_name"] = "Testy2" so_form["first_name"] = "Testy2"
# ao_pk is the initial pk of the authorizing official. set it before update # so_pk is the initial pk of the senior official. set it before update
# to be able to verify after update that the same contact object is in place # to be able to verify after update that the same contact object is in place
ao_pk = self.domain_information.authorizing_official.id so_pk = self.domain_information.senior_official.id
ao_form.submit() so_form.submit()
# refresh domain information # refresh domain information
self.domain_information.refresh_from_db() self.domain_information.refresh_from_db()
# assert that AO information is updated, and that the AO is a new Contact # assert that SO information is updated, and that the SO is a new Contact
self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) self.assertEqual("Testy2", self.domain_information.senior_official.first_name)
self.assertNotEqual(ao_pk, self.domain_information.authorizing_official.id) self.assertNotEqual(so_pk, self.domain_information.senior_official.id)
# assert that the Other Contact information is not updated and that the Other Contact # assert that the Other Contact information is not updated and that the Other Contact
# is the original Contact object # is the original Contact object
other_contact = self.domain_information.other_contacts.all()[0] other_contact = self.domain_information.other_contacts.all()[0]
self.assertEqual("Testy", other_contact.first_name) self.assertEqual("Testy", other_contact.first_name)
self.assertEqual(ao_pk, other_contact.id) self.assertEqual(so_pk, other_contact.id)
class TestDomainOrganization(TestDomainOverview): class TestDomainOrganization(TestDomainOverview):

View file

@ -3,6 +3,7 @@ from django.urls import reverse
from .test_views import TestWithUser from .test_views import TestWithUser
from django_webtest import WebTest # type: ignore from django_webtest import WebTest # type: ignore
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from api.tests.common import less_console_noise_decorator
class GetDomainsJsonTest(TestWithUser, WebTest): class GetDomainsJsonTest(TestWithUser, WebTest):
@ -11,9 +12,9 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
# Create test domains # Create test domains
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="active") self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown")
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="inactive") self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed")
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="active") self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
# Create UserDomainRoles # Create UserDomainRoles
UserDomainRole.objects.create(user=self.user, domain=self.domain1) UserDomainRole.objects.create(user=self.user, domain=self.domain1)
@ -25,6 +26,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
@less_console_noise_decorator
def test_get_domains_json_unauthenticated(self): def test_get_domains_json_unauthenticated(self):
"""for an unauthenticated user, test that the user is redirected for auth""" """for an unauthenticated user, test that the user is redirected for auth"""
self.app.reset() self.app.reset()
@ -32,6 +34,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
response = self.client.get(reverse("get_domains_json")) response = self.client.get(reverse("get_domains_json"))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@less_console_noise_decorator
def test_get_domains_json_authenticated(self): def test_get_domains_json_authenticated(self):
"""Test that an authenticated user gets the list of 3 domains.""" """Test that an authenticated user gets the list of 3 domains."""
response = self.app.get(reverse("get_domains_json")) response = self.app.get(reverse("get_domains_json"))
@ -102,6 +105,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
) )
self.assertEqual(svg_icon_expected, svg_icons[i]) self.assertEqual(svg_icon_expected, svg_icons[i])
@less_console_noise_decorator
def test_get_domains_json_search(self): def test_get_domains_json_search(self):
"""Test search.""" """Test search."""
# Define your URL variables as a dictionary # Define your URL variables as a dictionary
@ -131,6 +135,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
domains[0], domains[0],
) )
@less_console_noise_decorator
def test_pagination(self): def test_pagination(self):
"""Test that pagination is correct in the response""" """Test that pagination is correct in the response"""
response = self.app.get(reverse("get_domains_json"), {"page": 1}) response = self.app.get(reverse("get_domains_json"), {"page": 1})
@ -143,6 +148,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
self.assertFalse(data["has_previous"]) self.assertFalse(data["has_previous"])
self.assertEqual(data["num_pages"], 1) self.assertEqual(data["num_pages"], 1)
@less_console_noise_decorator
def test_sorting(self): def test_sorting(self):
"""test that sorting works properly in the response""" """test that sorting works properly in the response"""
response = self.app.get(reverse("get_domains_json"), {"sort_by": "expiration_date", "order": "desc"}) response = self.app.get(reverse("get_domains_json"), {"sort_by": "expiration_date", "order": "desc"})
@ -161,6 +167,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
expiration_dates = [domain["expiration_date"] for domain in data["domains"]] expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
self.assertEqual(expiration_dates, sorted(expiration_dates)) self.assertEqual(expiration_dates, sorted(expiration_dates))
@less_console_noise_decorator
def test_sorting_by_state_display(self): def test_sorting_by_state_display(self):
"""test that the state_display sorting works properly""" """test that the state_display sorting works properly"""
response = self.app.get(reverse("get_domains_json"), {"sort_by": "state_display", "order": "asc"}) response = self.app.get(reverse("get_domains_json"), {"sort_by": "state_display", "order": "asc"})
@ -178,3 +185,21 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
# Check if sorted by state_display in descending order # Check if sorted by state_display in descending order
states = [domain["state_display"] for domain in data["domains"]] states = [domain["state_display"] for domain in data["domains"]]
self.assertEqual(states, sorted(states, reverse=True)) self.assertEqual(states, sorted(states, reverse=True))
@less_console_noise_decorator
def test_state_filtering(self):
"""Test that different states in request get expected responses."""
expected_values = [
("unknown", 1),
("ready", 0),
("expired", 2),
("ready,expired", 2),
("unknown,expired", 3),
]
for state, num_domains in expected_values:
with self.subTest(state=state, num_domains=num_domains):
response = self.app.get(reverse("get_domains_json"), {"status": state})
self.assertEqual(response.status_code, 200)
data = response.json
self.assertEqual(len(data["domains"]), num_domains)

View file

@ -253,38 +253,38 @@ class DomainRequestTests(TestWithUser, WebTest):
# the post request should return a redirect to the next form in # the post request should return a redirect to the next form in
# the domain request page # the domain request page
self.assertEqual(org_contact_result.status_code, 302) self.assertEqual(org_contact_result.status_code, 302)
self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") self.assertEqual(org_contact_result["Location"], "/request/senior_official/")
num_pages_tested += 1 num_pages_tested += 1
# ---- AUTHORIZING OFFICIAL PAGE ---- # ---- SENIOR OFFICIAL PAGE ----
# Follow the redirect to the next form page # Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow() so_page = org_contact_result.follow()
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
ao_form["authorizing_official-first_name"] = "Testy ATO" so_form["senior_official-first_name"] = "Testy ATO"
ao_form["authorizing_official-last_name"] = "Tester ATO" so_form["senior_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester" so_form["senior_official-title"] = "Chief Tester"
ao_form["authorizing_official-email"] = "testy@town.com" so_form["senior_official-email"] = "testy@town.com"
# test next button # test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_result = ao_form.submit() so_result = so_form.submit()
# validate that data from this step are being saved # validate that data from this step are being saved
domain_request = DomainRequest.objects.get() # there's only one domain_request = DomainRequest.objects.get() # there's only one
self.assertEqual(domain_request.authorizing_official.first_name, "Testy ATO") self.assertEqual(domain_request.senior_official.first_name, "Testy ATO")
self.assertEqual(domain_request.authorizing_official.last_name, "Tester ATO") self.assertEqual(domain_request.senior_official.last_name, "Tester ATO")
self.assertEqual(domain_request.authorizing_official.title, "Chief Tester") self.assertEqual(domain_request.senior_official.title, "Chief Tester")
self.assertEqual(domain_request.authorizing_official.email, "testy@town.com") self.assertEqual(domain_request.senior_official.email, "testy@town.com")
# the post request should return a redirect to the next form in # the post request should return a redirect to the next form in
# the domain request page # the domain request page
self.assertEqual(ao_result.status_code, 302) self.assertEqual(so_result.status_code, 302)
self.assertEqual(ao_result["Location"], "/request/current_sites/") self.assertEqual(so_result["Location"], "/request/current_sites/")
num_pages_tested += 1 num_pages_tested += 1
# ---- CURRENT SITES PAGE ---- # ---- CURRENT SITES PAGE ----
# Follow the redirect to the next form page # Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = ao_result.follow() current_sites_page = so_result.follow()
current_sites_form = current_sites_page.forms[0] current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.city.com" current_sites_form["current_sites-0-website"] = "www.city.com"
@ -610,38 +610,38 @@ class DomainRequestTests(TestWithUser, WebTest):
# the post request should return a redirect to the next form in # the post request should return a redirect to the next form in
# the domain request page # the domain request page
self.assertEqual(org_contact_result.status_code, 302) self.assertEqual(org_contact_result.status_code, 302)
self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") self.assertEqual(org_contact_result["Location"], "/request/senior_official/")
num_pages_tested += 1 num_pages_tested += 1
# ---- AUTHORIZING OFFICIAL PAGE ---- # ---- SENIOR OFFICIAL PAGE ----
# Follow the redirect to the next form page # Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow() so_page = org_contact_result.follow()
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
ao_form["authorizing_official-first_name"] = "Testy ATO" so_form["senior_official-first_name"] = "Testy ATO"
ao_form["authorizing_official-last_name"] = "Tester ATO" so_form["senior_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester" so_form["senior_official-title"] = "Chief Tester"
ao_form["authorizing_official-email"] = "testy@town.com" so_form["senior_official-email"] = "testy@town.com"
# test next button # test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_result = ao_form.submit() so_result = so_form.submit()
# validate that data from this step are being saved # validate that data from this step are being saved
domain_request = DomainRequest.objects.get() # there's only one domain_request = DomainRequest.objects.get() # there's only one
self.assertEqual(domain_request.authorizing_official.first_name, "Testy ATO") self.assertEqual(domain_request.senior_official.first_name, "Testy ATO")
self.assertEqual(domain_request.authorizing_official.last_name, "Tester ATO") self.assertEqual(domain_request.senior_official.last_name, "Tester ATO")
self.assertEqual(domain_request.authorizing_official.title, "Chief Tester") self.assertEqual(domain_request.senior_official.title, "Chief Tester")
self.assertEqual(domain_request.authorizing_official.email, "testy@town.com") self.assertEqual(domain_request.senior_official.email, "testy@town.com")
# the post request should return a redirect to the next form in # the post request should return a redirect to the next form in
# the domain request page # the domain request page
self.assertEqual(ao_result.status_code, 302) self.assertEqual(so_result.status_code, 302)
self.assertEqual(ao_result["Location"], "/request/current_sites/") self.assertEqual(so_result["Location"], "/request/current_sites/")
num_pages_tested += 1 num_pages_tested += 1
# ---- CURRENT SITES PAGE ---- # ---- CURRENT SITES PAGE ----
# Follow the redirect to the next form page # Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = ao_result.follow() current_sites_page = so_result.follow()
current_sites_form = current_sites_page.forms[0] current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.city.com" current_sites_form["current_sites-0-website"] = "www.city.com"
@ -1576,7 +1576,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Populate the database with a domain request that # Populate the database with a domain request that
# has 1 "other contact" assigned to it # has 1 "other contact" assigned to it
# We'll do it from scratch so we can reuse the other contact # We'll do it from scratch so we can reuse the other contact
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -1607,7 +1607,7 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
submitter=you, submitter=you,
creator=self.user, creator=self.user,
status="started", status="started",
@ -1703,7 +1703,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Populate the database with a domain request that # Populate the database with a domain request that
# has 2 "other contact" assigned to it # has 2 "other contact" assigned to it
# We'll do it from scratch so we can reuse the other contact # We'll do it from scratch so we can reuse the other contact
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -1741,7 +1741,7 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
submitter=you, submitter=you,
creator=self.user, creator=self.user,
status="started", status="started",
@ -1784,7 +1784,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Populate the database with a domain request that # Populate the database with a domain request that
# has 1 "other contact" assigned to it # has 1 "other contact" assigned to it
# We'll do it from scratch so we can reuse the other contact # We'll do it from scratch so we can reuse the other contact
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -1815,7 +1815,7 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
submitter=you, submitter=you,
creator=self.user, creator=self.user,
status="started", status="started",
@ -1861,7 +1861,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Populate the database with a domain request that # Populate the database with a domain request that
# has 1 "other contact" assigned to it # has 1 "other contact" assigned to it
# We'll do it from scratch so we can reuse the other contact # We'll do it from scratch so we can reuse the other contact
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -1892,7 +1892,7 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
submitter=you, submitter=you,
creator=self.user, creator=self.user,
status="started", status="started",
@ -1937,7 +1937,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Populate the database with a domain request that # Populate the database with a domain request that
# has 1 "other contact" assigned to it # has 1 "other contact" assigned to it
# We'll do it from scratch # We'll do it from scratch
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -1968,7 +1968,7 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
submitter=you, submitter=you,
creator=self.user, creator=self.user,
status="started", status="started",
@ -2017,9 +2017,9 @@ class DomainRequestTests(TestWithUser, WebTest):
# Populate the database with a domain request that # Populate the database with a domain request that
# has 1 "other contact" assigned to it, the other contact is also # has 1 "other contact" assigned to it, the other contact is also
# the authorizing official initially # the senior official initially
# We'll do it from scratch # We'll do it from scratch
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -2043,17 +2043,17 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
submitter=you, submitter=you,
creator=self.user, creator=self.user,
status="started", status="started",
) )
domain_request.other_contacts.add(ao) domain_request.other_contacts.add(so)
# other_contact_pk is the initial pk of the other contact. set it before update # other_contact_pk is the initial pk of the other contact. set it before update
# to be able to verify after update that the ao contact is still in place # to be able to verify after update that the so contact is still in place
# and not updated, and that the new contact has a new id # and not updated, and that the new contact has a new id
other_contact_pk = ao.id other_contact_pk = so.id
# prime the form by visiting /edit # prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
@ -2085,20 +2085,20 @@ class DomainRequestTests(TestWithUser, WebTest):
other_contact = domain_request.other_contacts.all()[0] other_contact = domain_request.other_contacts.all()[0]
self.assertNotEquals(other_contact_pk, other_contact.id) self.assertNotEquals(other_contact_pk, other_contact.id)
self.assertEquals("Testy2", other_contact.first_name) self.assertEquals("Testy2", other_contact.first_name)
# assert that the authorizing official is not updated # assert that the senior official is not updated
authorizing_official = domain_request.authorizing_official senior_official = domain_request.senior_official
self.assertEquals("Testy", authorizing_official.first_name) self.assertEquals("Testy", senior_official.first_name)
def test_edit_authorizing_official_in_place(self): def test_edit_senior_official_in_place(self):
"""When you: """When you:
1. edit an authorizing official which is not joined to another model, 1. edit a senior official which is not joined to another model,
2. then submit, 2. then submit,
the domain request is linked to the existing ao, and the ao updated.""" the domain request is linked to the existing so, and the so updated."""
# Populate the database with a domain request that # Populate the database with a domain request that
# has an authorizing_official (ao) # has a senior_official (so)
# We'll do it from scratch # We'll do it from scratch
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -2115,14 +2115,14 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
creator=self.user, creator=self.user,
status="started", status="started",
) )
# ao_pk is the initial pk of the Authorizing Official. set it before update # so_pk is the initial pk of the Senior Official. set it before update
# to be able to verify after update that the same Contact object is in place # to be able to verify after update that the same Contact object is in place
ao_pk = ao.id so_pk = so.id
# prime the form by visiting /edit # prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
@ -2133,38 +2133,38 @@ class DomainRequestTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = self.app.get(reverse("domain-request:authorizing_official")) so_page = self.app.get(reverse("domain-request:senior_official"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
# Minimal check to ensure the form is loaded # Minimal check to ensure the form is loaded
self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") self.assertEqual(so_form["senior_official-first_name"].value, "Testy")
# update the first name of the contact # update the first name of the contact
ao_form["authorizing_official-first_name"] = "Testy2" so_form["senior_official-first_name"] = "Testy2"
# Submit the updated form # Submit the updated form
ao_form.submit() so_form.submit()
domain_request.refresh_from_db() domain_request.refresh_from_db()
# assert AO is updated "in place" # assert SO is updated "in place"
updated_ao = domain_request.authorizing_official updated_so = domain_request.senior_official
self.assertEquals(ao_pk, updated_ao.id) self.assertEquals(so_pk, updated_so.id)
self.assertEquals("Testy2", updated_ao.first_name) self.assertEquals("Testy2", updated_so.first_name)
def test_edit_authorizing_official_creates_new(self): def test_edit_senior_official_creates_new(self):
"""When you: """When you:
1. edit an existing authorizing official which IS joined to another model, 1. edit an existing senior official which IS joined to another model,
2. then submit, 2. then submit,
the domain request is linked to a new Contact, and the new Contact is updated.""" the domain request is linked to a new Contact, and the new Contact is updated."""
# Populate the database with a domain request that # Populate the database with a domain request that
# has authorizing official assigned to it, the authorizing offical is also # has senior official assigned to it, the senior offical is also
# an other contact initially # an other contact initially
# We'll do it from scratch # We'll do it from scratch
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -2181,16 +2181,16 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
creator=self.user, creator=self.user,
status="started", status="started",
) )
domain_request.other_contacts.add(ao) domain_request.other_contacts.add(so)
# ao_pk is the initial pk of the authorizing official. set it before update # so_pk is the initial pk of the senior official. set it before update
# to be able to verify after update that the other contact is still in place # to be able to verify after update that the other contact is still in place
# and not updated, and that the new ao has a new id # and not updated, and that the new so has a new id
ao_pk = ao.id so_pk = so.id
# prime the form by visiting /edit # prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})) self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
@ -2201,30 +2201,30 @@ class DomainRequestTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = self.app.get(reverse("domain-request:authorizing_official")) so_page = self.app.get(reverse("domain-request:senior_official"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
# Minimal check to ensure the form is loaded # Minimal check to ensure the form is loaded
self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") self.assertEqual(so_form["senior_official-first_name"].value, "Testy")
# update the first name of the contact # update the first name of the contact
ao_form["authorizing_official-first_name"] = "Testy2" so_form["senior_official-first_name"] = "Testy2"
# Submit the updated form # Submit the updated form
ao_form.submit() so_form.submit()
domain_request.refresh_from_db() domain_request.refresh_from_db()
# assert that the other contact is not updated # assert that the other contact is not updated
other_contacts = domain_request.other_contacts.all() other_contacts = domain_request.other_contacts.all()
other_contact = other_contacts[0] other_contact = other_contacts[0]
self.assertEquals(ao_pk, other_contact.id) self.assertEquals(so_pk, other_contact.id)
self.assertEquals("Testy", other_contact.first_name) self.assertEquals("Testy", other_contact.first_name)
# assert that the authorizing official is updated # assert that the senior official is updated
authorizing_official = domain_request.authorizing_official senior_official = domain_request.senior_official
self.assertEquals("Testy2", authorizing_official.first_name) self.assertEquals("Testy2", senior_official.first_name)
def test_edit_submitter_in_place(self): def test_edit_submitter_in_place(self):
"""When you: """When you:
@ -2421,7 +2421,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# and the step is on the sidebar list. # and the step is on the sidebar list.
self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT])
def test_domain_request_ao_dynamic_text(self): def test_domain_request_so_dynamic_text(self):
intro_page = self.app.get(reverse("domain-request:")) intro_page = self.app.get(reverse("domain-request:"))
# django-webtest does not handle cookie-based sessions well because it keeps # django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept # resetting the session key on each new request, thus destroying the concept
@ -2474,24 +2474,24 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_result = org_contact_form.submit() org_contact_result = org_contact_form.submit()
# ---- AO CONTACT PAGE ---- # ---- SO CONTACT PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow() so_page = org_contact_result.follow()
self.assertContains(ao_page, "Executive branch federal agencies") self.assertContains(so_page, "Executive branch federal agencies")
# Go back to organization type page and change type # Go back to organization type page and change type
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page.click(str(self.TITLES["generic_org_type"]), index=0) so_page.click(str(self.TITLES["generic_org_type"]), index=0)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_form["generic_org_type-generic_org_type"] = "city" type_form["generic_org_type-generic_org_type"] = "city"
type_result = type_form.submit() type_result = type_form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
election_page = type_result.follow() election_page = type_result.follow()
# Go back to AO page and test the dynamic text changed # Go back to SO page and test the dynamic text changed
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = election_page.click(str(self.TITLES["authorizing_official"]), index=0) so_page = election_page.click(str(self.TITLES["senior_official"]), index=0)
self.assertContains(ao_page, "Domain requests from cities") self.assertContains(so_page, "Domain requests from cities")
def test_domain_request_dotgov_domain_dynamic_text(self): def test_domain_request_dotgov_domain_dynamic_text(self):
intro_page = self.app.get(reverse("domain-request:")) intro_page = self.app.get(reverse("domain-request:"))
@ -2546,27 +2546,27 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_result = org_contact_form.submit() org_contact_result = org_contact_form.submit()
# ---- AO CONTACT PAGE ---- # ---- SO CONTACT PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow() so_page = org_contact_result.follow()
# ---- AUTHORIZING OFFICIAL PAGE ---- # ---- senior OFFICIAL PAGE ----
# Follow the redirect to the next form page # Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow() so_page = org_contact_result.follow()
ao_form = ao_page.forms[0] so_form = so_page.forms[0]
ao_form["authorizing_official-first_name"] = "Testy ATO" so_form["senior_official-first_name"] = "Testy ATO"
ao_form["authorizing_official-last_name"] = "Tester ATO" so_form["senior_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester" so_form["senior_official-title"] = "Chief Tester"
ao_form["authorizing_official-email"] = "testy@town.com" so_form["senior_official-email"] = "testy@town.com"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_result = ao_form.submit() so_result = so_form.submit()
# ---- CURRENT SITES PAGE ---- # ---- CURRENT SITES PAGE ----
# Follow the redirect to the next form page # Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = ao_result.follow() current_sites_page = so_result.follow()
current_sites_form = current_sites_page.forms[0] current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.city.com" current_sites_form["current_sites-0-website"] = "www.city.com"
@ -2627,7 +2627,7 @@ class DomainRequestTests(TestWithUser, WebTest):
""" """
Test that a previously saved domain request is available at the /edit endpoint. Test that a previously saved domain request is available at the /edit endpoint.
""" """
ao, _ = Contact.objects.get_or_create( so, _ = Contact.objects.get_or_create(
first_name="Testy", first_name="Testy",
last_name="Tester", last_name="Tester",
title="Chief Tester", title="Chief Tester",
@ -2661,7 +2661,7 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1", address_line1="address 1",
state_territory="NY", state_territory="NY",
zipcode="10002", zipcode="10002",
authorizing_official=ao, senior_official=so,
requested_domain=domain, requested_domain=domain,
submitter=you, submitter=you,
creator=self.user, creator=self.user,
@ -2699,7 +2699,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# page = self.app.get(url) # page = self.app.get(url)
# self.assertNotContains(page, "VALUE") # self.assertNotContains(page, "VALUE")
# url = reverse("domain-request:authorizing_official") # url = reverse("domain-request:senior_official")
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url) # page = self.app.get(url)
# self.assertNotContains(page, "VALUE") # self.assertNotContains(page, "VALUE")
@ -2981,7 +2981,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
creator=self.user, creator=self.user,
requested_domain=site, requested_domain=site,
status=DomainRequest.DomainRequestStatus.WITHDRAWN, status=DomainRequest.DomainRequestStatus.WITHDRAWN,
authorizing_official=contact, senior_official=contact,
submitter=contact_user, submitter=contact_user,
) )
domain_request.other_contacts.set([contact_2]) domain_request.other_contacts.set([contact_2])
@ -3008,7 +3008,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
# Now 'detail_page' contains the response after following the redirect # Now 'detail_page' contains the response after following the redirect
self.assertEqual(detail_page.status_code, 200) self.assertEqual(detail_page.status_code, 200)
# 5 unlocked steps (ao, domain, submitter, other contacts, and current sites # 5 unlocked steps (so, domain, submitter, other contacts, and current sites
# which unlocks if domain exists), one active step, the review step is locked # which unlocks if domain exists), one active step, the review step is locked
self.assertContains(detail_page, "#check_circle", count=5) self.assertContains(detail_page, "#check_circle", count=5)
# Type of organization # Type of organization

View file

@ -40,20 +40,20 @@ def get_domain_infos(filter_condition, sort_fields):
returns: A queryset of DomainInformation objects returns: A queryset of DomainInformation objects
""" """
domain_infos = ( domain_infos = (
DomainInformation.objects.select_related("domain", "authorizing_official") DomainInformation.objects.select_related("domain", "senior_official")
.filter(**filter_condition) .filter(**filter_condition)
.order_by(*sort_fields) .order_by(*sort_fields)
.distinct() .distinct()
) )
# Do a mass concat of the first and last name fields for authorizing_official. # Do a mass concat of the first and last name fields for senior_official.
# The old operation was computationally heavy for some reason, so if we precompute # The old operation was computationally heavy for some reason, so if we precompute
# this here, it is vastly more efficient. # this here, it is vastly more efficient.
domain_infos_cleaned = domain_infos.annotate( domain_infos_cleaned = domain_infos.annotate(
ao=Concat( so=Concat(
Coalesce(F("authorizing_official__first_name"), Value("")), Coalesce(F("senior_official__first_name"), Value("")),
Value(" "), Value(" "),
Coalesce(F("authorizing_official__last_name"), Value("")), Coalesce(F("senior_official__last_name"), Value("")),
output_field=CharField(), output_field=CharField(),
) )
) )
@ -110,8 +110,8 @@ def parse_row_for_domain(
"Organization name": domain_info.organization_name, "Organization name": domain_info.organization_name,
"City": domain_info.city, "City": domain_info.city,
"State": domain_info.state_territory, "State": domain_info.state_territory,
"AO": domain_info.ao, # type: ignore "SO": domain_info.so, # type: ignore
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ", "SO email": domain_info.senior_official.email if domain_info.senior_official else " ",
"Security contact email": security_email, "Security contact email": security_email,
"Created at": domain.created_at, "Created at": domain.created_at,
"Deleted": domain.deleted, "Deleted": domain.deleted,
@ -325,8 +325,8 @@ def export_data_type_to_csv(csv_file):
"Organization name", "Organization name",
"City", "City",
"State", "State",
"AO", "SO",
"AO email", "SO email",
"Security contact email", "Security contact email",
# For domain manager we are pass it in as a parameter below in write_body # For domain manager we are pass it in as a parameter below in write_body
] ]
@ -728,10 +728,10 @@ class DomainRequestExport:
"Creator approved domains count", "Creator approved domains count",
"Creator active requests count", "Creator active requests count",
"Alternative domains", "Alternative domains",
"AO first name", "SO first name",
"AO last name", "SO last name",
"AO email", "SO email",
"AO title/role", "SO title/role",
"Request purpose", "Request purpose",
"Request additional details", "Request additional details",
"Other contacts", "Other contacts",
@ -798,7 +798,7 @@ class DomainRequestExport:
requests = ( requests = (
DomainRequest.objects.select_related( DomainRequest.objects.select_related(
"creator", "authorizing_official", "federal_agency", "investigator", "requested_domain" "creator", "senior_official", "federal_agency", "investigator", "requested_domain"
) )
.prefetch_related("current_websites", "other_contacts", "alternative_domains") .prefetch_related("current_websites", "other_contacts", "alternative_domains")
.exclude(status__in=[DomainRequest.DomainRequestStatus.STARTED]) .exclude(status__in=[DomainRequest.DomainRequestStatus.STARTED])
@ -817,10 +817,10 @@ class DomainRequestExport:
additional_values = [ additional_values = [
"requested_domain__name", "requested_domain__name",
"federal_agency__agency", "federal_agency__agency",
"authorizing_official__first_name", "senior_official__first_name",
"authorizing_official__last_name", "senior_official__last_name",
"authorizing_official__email", "senior_official__email",
"authorizing_official__title", "senior_official__title",
"creator__first_name", "creator__first_name",
"creator__last_name", "creator__last_name",
"creator__email", "creator__email",
@ -940,10 +940,10 @@ class DomainRequestExport:
"Current websites": request.get("all_current_websites"), "Current websites": request.get("all_current_websites"),
# Untouched FK fields - passed into the request dict. # Untouched FK fields - passed into the request dict.
"Federal agency": request.get("federal_agency__agency"), "Federal agency": request.get("federal_agency__agency"),
"AO first name": request.get("authorizing_official__first_name"), "SO first name": request.get("senior_official__first_name"),
"AO last name": request.get("authorizing_official__last_name"), "SO last name": request.get("senior_official__last_name"),
"AO email": request.get("authorizing_official__email"), "SO email": request.get("senior_official__email"),
"AO title/role": request.get("authorizing_official__title"), "SO title/role": request.get("senior_official__title"),
"Creator first name": request.get("creator__first_name"), "Creator first name": request.get("creator__first_name"),
"Creator last name": request.get("creator__last_name"), "Creator last name": request.get("creator__last_name"),
"Creator email": request.get("creator__email"), "Creator email": request.get("creator__email"),

View file

@ -1,7 +1,7 @@
from .domain_request import * from .domain_request import *
from .domain import ( from .domain import (
DomainView, DomainView,
DomainAuthorizingOfficialView, DomainSeniorOfficialView,
DomainOrgNameAddressView, DomainOrgNameAddressView,
DomainDNSView, DomainDNSView,
DomainNameserversView, DomainNameserversView,

View file

@ -41,7 +41,7 @@ from registrar.views.utility.permission_views import UserDomainRolePermissionDel
from ..forms import ( from ..forms import (
ContactForm, ContactForm,
AuthorizingOfficialContactForm, SeniorOfficialContactForm,
DomainOrgNameAddressForm, DomainOrgNameAddressForm,
DomainAddUserForm, DomainAddUserForm,
DomainSecurityEmailForm, DomainSecurityEmailForm,
@ -228,18 +228,18 @@ class DomainOrgNameAddressView(DomainFormBaseView):
return super().form_valid(form) return super().form_valid(form)
class DomainAuthorizingOfficialView(DomainFormBaseView): class DomainSeniorOfficialView(DomainFormBaseView):
"""Domain authorizing official editing view.""" """Domain senior official editing view."""
model = Domain model = Domain
template_name = "domain_authorizing_official.html" template_name = "domain_senior_official.html"
context_object_name = "domain" context_object_name = "domain"
form_class = AuthorizingOfficialContactForm form_class = SeniorOfficialContactForm
def get_form_kwargs(self, *args, **kwargs): def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.authorizing_official instance to make a bound form.""" """Add domain_info.senior_official instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs) form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.object.domain_info.authorizing_official form_kwargs["instance"] = self.object.domain_info.senior_official
domain_info = self.get_domain_info_from_domain() domain_info = self.get_domain_info_from_domain()
invalid_fields = [DomainRequest.OrganizationChoices.FEDERAL, DomainRequest.OrganizationChoices.TRIBAL] invalid_fields = [DomainRequest.OrganizationChoices.FEDERAL, DomainRequest.OrganizationChoices.TRIBAL]
@ -256,10 +256,10 @@ class DomainAuthorizingOfficialView(DomainFormBaseView):
def get_success_url(self): def get_success_url(self):
"""Redirect to the overview page for the domain.""" """Redirect to the overview page for the domain."""
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk}) return reverse("domain-senior-official", kwargs={"pk": self.object.pk})
def form_valid(self, form): def form_valid(self, form):
"""The form is valid, save the authorizing official.""" """The form is valid, save the senior official."""
# Set the domain information in the form so that it can be accessible # Set the domain information in the form so that it can be accessible
# to associate a new Contact, if a new Contact is needed # to associate a new Contact, if a new Contact is needed
@ -267,7 +267,7 @@ class DomainAuthorizingOfficialView(DomainFormBaseView):
form.set_domain_info(self.object.domain_info) form.set_domain_info(self.object.domain_info)
form.save() form.save()
messages.success(self.request, "The authorizing official for this domain has been updated.") messages.success(self.request, "The senior official for this domain has been updated.")
# superclass has the redirect # superclass has the redirect
return super().form_valid(form) return super().form_valid(form)

View file

@ -41,7 +41,7 @@ class Step(StrEnum):
ORGANIZATION_ELECTION = "organization_election" ORGANIZATION_ELECTION = "organization_election"
ORGANIZATION_CONTACT = "organization_contact" ORGANIZATION_CONTACT = "organization_contact"
ABOUT_YOUR_ORGANIZATION = "about_your_organization" ABOUT_YOUR_ORGANIZATION = "about_your_organization"
AUTHORIZING_OFFICIAL = "authorizing_official" SENIOR_OFFICIAL = "senior_official"
CURRENT_SITES = "current_sites" CURRENT_SITES = "current_sites"
DOTGOV_DOMAIN = "dotgov_domain" DOTGOV_DOMAIN = "dotgov_domain"
PURPOSE = "purpose" PURPOSE = "purpose"
@ -87,7 +87,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
Step.ORGANIZATION_ELECTION: _("Election office"), Step.ORGANIZATION_ELECTION: _("Election office"),
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"), Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"), Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
Step.AUTHORIZING_OFFICIAL: _("Authorizing official"), Step.SENIOR_OFFICIAL: _("Senior official"),
Step.CURRENT_SITES: _("Current websites"), Step.CURRENT_SITES: _("Current websites"),
Step.DOTGOV_DOMAIN: _(".gov domain"), Step.DOTGOV_DOMAIN: _(".gov domain"),
Step.PURPOSE: _("Purpose of your domain"), Step.PURPOSE: _("Purpose of your domain"),
@ -358,7 +358,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
or self.domain_request.urbanization is not None or self.domain_request.urbanization is not None
), ),
"about_your_organization": self.domain_request.about_your_organization is not None, "about_your_organization": self.domain_request.about_your_organization is not None,
"authorizing_official": self.domain_request.authorizing_official is not None, "senior_official": self.domain_request.senior_official is not None,
"current_sites": ( "current_sites": (
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
), ),
@ -540,9 +540,9 @@ class AboutYourOrganization(DomainRequestWizard):
forms = [forms.AboutYourOrganizationForm] forms = [forms.AboutYourOrganizationForm]
class AuthorizingOfficial(DomainRequestWizard): class SeniorOfficial(DomainRequestWizard):
template_name = "domain_request_authorizing_official.html" template_name = "domain_request_senior_official.html"
forms = [forms.AuthorizingOfficialForm] forms = [forms.SeniorOfficialForm]
def get_context_data(self): def get_context_data(self):
context = super().get_context_data() context = super().get_context_data()
@ -817,7 +817,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
# After a delete occurs, do a second sweep on any returned duplicates. # After a delete occurs, do a second sweep on any returned duplicates.
# This determines if any of these three fields share a contact, which is used for # This determines if any of these three fields share a contact, which is used for
# the edge case where the same user may be an AO, and a submitter, for example. # the edge case where the same user may be an SO, and a submitter, for example.
if len(duplicates) > 0: if len(duplicates) > 0:
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True) duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete() Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
@ -830,7 +830,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
Collects all orphaned contacts associated with a given DomainRequest object. Collects all orphaned contacts associated with a given DomainRequest object.
An orphaned contact is defined as a contact that is associated with the domain request, An orphaned contact is defined as a contact that is associated with the domain request,
but not with any other domain_request. This includes the authorizing official, the submitter, but not with any other domain_request. This includes the senior official, the submitter,
and any other contacts linked to the domain_request. and any other contacts linked to the domain_request.
Parameters: Parameters:
@ -845,19 +845,19 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
contacts_to_delete = [] contacts_to_delete = []
# Get each contact object on the DomainRequest object # Get each contact object on the DomainRequest object
ao = domain_request.authorizing_official so = domain_request.senior_official
submitter = domain_request.submitter submitter = domain_request.submitter
other_contacts = list(domain_request.other_contacts.all()) other_contacts = list(domain_request.other_contacts.all())
other_contact_ids = domain_request.other_contacts.all().values_list("id", flat=True) other_contact_ids = domain_request.other_contacts.all().values_list("id", flat=True)
# Check if the desired item still exists in the DB # Check if the desired item still exists in the DB
if check_db: if check_db:
ao = self._get_contacts_by_id([ao.id]).first() if ao is not None else None so = self._get_contacts_by_id([so.id]).first() if so is not None else None
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
other_contacts = self._get_contacts_by_id(other_contact_ids) other_contacts = self._get_contacts_by_id(other_contact_ids)
# Pair each contact with its db related name for use in checking if it has joins # Pair each contact with its db related name for use in checking if it has joins
checked_contacts = [(ao, "authorizing_official"), (submitter, "submitted_domain_requests")] checked_contacts = [(so, "senior_official"), (submitter, "submitted_domain_requests")]
checked_contacts.extend((contact, "contact_domain_requests") for contact in other_contacts) checked_contacts.extend((contact, "contact_domain_requests") for contact in other_contacts)
for contact, related_name in checked_contacts: for contact, related_name in checked_contacts:

View file

@ -20,11 +20,46 @@ def get_domains_json(request):
# Handle sorting # Handle sorting
sort_by = request.GET.get("sort_by", "id") # Default to 'id' sort_by = request.GET.get("sort_by", "id") # Default to 'id'
order = request.GET.get("order", "asc") # Default to 'asc' order = request.GET.get("order", "asc") # Default to 'asc'
search_term = request.GET.get("search_term")
# Handle search term
search_term = request.GET.get("search_term")
if search_term: if search_term:
objects = objects.filter(Q(name__icontains=search_term)) objects = objects.filter(Q(name__icontains=search_term))
# Handle state
status_param = request.GET.get("status")
if status_param:
status_list = status_param.split(",")
# if unknown is in status_list, append 'dns needed' since both
# unknown and dns needed display as DNS Needed, and both are
# searchable via state parameter of 'unknown'
if "unknown" in status_list:
status_list.append("dns needed")
# Split the status list into normal states and custom states
normal_states = [state for state in status_list if state in Domain.State.values]
custom_states = [state for state in status_list if state == "expired"]
# Construct Q objects for normal states that can be queried through ORM
state_query = Q()
if normal_states:
state_query |= Q(state__in=normal_states)
# Handle custom states in Python, as expired can not be queried through ORM
if "expired" in custom_states:
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
state_query |= Q(id__in=expired_domain_ids)
# Apply the combined query
objects = objects.filter(state_query)
# If there are filtered states, and expired is not one of them, domains with
# state_display of 'Expired' must be removed
if "expired" not in custom_states:
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
objects = objects.exclude(id__in=expired_domain_ids)
if sort_by == "state_display": if sort_by == "state_display":
# Fetch the objects and sort them in Python # Fetch the objects and sort them in Python
objects = list(objects) # Evaluate queryset to a list objects = list(objects) # Evaluate queryset to a list

View file

@ -57,7 +57,7 @@
10038 OUTOFSCOPE http://app:8080/users/add 10038 OUTOFSCOPE http://app:8080/users/add
10038 OUTOFSCOPE http://app:8080/nameservers 10038 OUTOFSCOPE http://app:8080/nameservers
10038 OUTOFSCOPE http://app:8080/your-contact-information 10038 OUTOFSCOPE http://app:8080/your-contact-information
10038 OUTOFSCOPE http://app:8080/authorizing-official 10038 OUTOFSCOPE http://app:8080/senior-official
10038 OUTOFSCOPE http://app:8080/security-email 10038 OUTOFSCOPE http://app:8080/security-email
10038 OUTOFSCOPE http://app:8080/delete 10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw 10038 OUTOFSCOPE http://app:8080/withdraw
@ -68,6 +68,8 @@
10038 OUTOFSCOPE http://app:8080/dns/dnssec 10038 OUTOFSCOPE http://app:8080/dns/dnssec
10038 OUTOFSCOPE http://app:8080/dns/dnssec/dsdata 10038 OUTOFSCOPE http://app:8080/dns/dnssec/dsdata
10038 OUTOFSCOPE http://app:8080/org-name-address 10038 OUTOFSCOPE http://app:8080/org-name-address
10038 OUTOFSCOPE http://app:8080/domain_requests/
10038 OUTOFSCOPE http://app:8080/domains/
# This URL always returns 404, so include it as well. # This URL always returns 404, so include it as well.
10038 OUTOFSCOPE http://app:8080/todo 10038 OUTOFSCOPE http://app:8080/todo
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers # OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers