diff --git a/docs/architecture/decisions/0026-django-waffle-library.md b/docs/architecture/decisions/0026-django-waffle-library.md new file mode 100644 index 000000000..256aaf5ed --- /dev/null +++ b/docs/architecture/decisions/0026-django-waffle-library.md @@ -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. \ No newline at end of file diff --git a/docs/architecture/diagrams/model_timeline.md b/docs/architecture/diagrams/model_timeline.md index 7baf611ad..f05b9e056 100644 --- a/docs/architecture/diagrams/model_timeline.md +++ b/docs/architecture/diagrams/model_timeline.md @@ -41,7 +41,7 @@ class DomainRequest { -- creator (User) investigator (User) - authorizing_official (Contact) + senior_official (Contact) submitter (Contact) other_contacts (Contacts) 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 { Requested domain diff --git a/docs/architecture/diagrams/models_diagram.md b/docs/architecture/diagrams/models_diagram.md index 837906934..455f8fb09 100644 --- a/docs/architecture/diagrams/models_diagram.md +++ b/docs/architecture/diagrams/models_diagram.md @@ -20,7 +20,7 @@ docker compose exec app ./manage.py generate_puml --include registrar ## 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` -2. Run the following command +2. Navigate to the `diagram` folder and then run the following command below: ```bash docker run -v $(pwd):$(pwd) -w $(pwd) -it plantuml/plantuml -tsvg models_diagram.md @@ -103,6 +103,21 @@ class "registrar.PublicContact " as registrar.PublicContact #d6f4e9 { registrar.PublicContact -- registrar.Domain +class "registrar.UserDomainRole " 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 " as registrar.Domain #d6f4e9 { domain -- @@ -115,6 +130,7 @@ class "registrar.Domain " as registrar.Domain #d6f4e9 { + security_contact_registry_id (TextField) + deleted (DateField) + first_ready (DateField) + + dsdata_last_change (TextField) -- } @@ -126,6 +142,7 @@ class "registrar.FederalAgency " as registrar.FederalAgency #d6f4e9 { + created_at (DateTimeField) + updated_at (DateTimeField) + agency (CharField) + + federal_type (CharField) -- } @@ -138,7 +155,10 @@ class "registrar.DomainRequest " as registrar.DomainRequest #d6f4e9 { + updated_at (DateTimeField) + status (FSMField) + rejection_reason (TextField) + + action_needed_reason (TextField) + + action_needed_reason_email (TextField) ~ federal_agency (ForeignKey) + ~ portfolio (ForeignKey) ~ creator (ForeignKey) ~ investigator (ForeignKey) + generic_org_type (CharField) @@ -156,7 +176,7 @@ class "registrar.DomainRequest " as registrar.DomainRequest #d6f4e9 { + zipcode (CharField) + urbanization (CharField) + about_your_organization (TextField) - ~ authorizing_official (ForeignKey) + ~ senior_official (ForeignKey) ~ approved_domain (OneToOneField) ~ requested_domain (OneToOneField) ~ submitter (ForeignKey) @@ -165,6 +185,8 @@ class "registrar.DomainRequest " as registrar.DomainRequest #d6f4e9 { + anything_else (TextField) + has_anything_else_text (BooleanField) + cisa_representative_email (EmailField) + + cisa_representative_first_name (CharField) + + cisa_representative_last_name (CharField) + has_cisa_representative (BooleanField) + is_policy_acknowledged (BooleanField) + submission_date (DateField) @@ -175,6 +197,7 @@ class "registrar.DomainRequest " as registrar.DomainRequest #d6f4e9 { -- } registrar.DomainRequest -- registrar.FederalAgency +registrar.DomainRequest -- registrar.Portfolio registrar.DomainRequest -- registrar.User registrar.DomainRequest -- registrar.User registrar.DomainRequest -- registrar.Contact @@ -194,6 +217,7 @@ class "registrar.DomainInformation " as registrar.DomainInformation # + updated_at (DateTimeField) ~ federal_agency (ForeignKey) ~ creator (ForeignKey) + ~ portfolio (ForeignKey) ~ domain_request (OneToOneField) + generic_org_type (CharField) + organization_type (CharField) @@ -210,13 +234,17 @@ class "registrar.DomainInformation " as registrar.DomainInformation # + zipcode (CharField) + urbanization (CharField) + about_your_organization (TextField) - ~ authorizing_official (ForeignKey) + ~ senior_official (ForeignKey) ~ domain (OneToOneField) ~ submitter (ForeignKey) + purpose (TextField) + no_other_contacts_rationale (TextField) + anything_else (TextField) + + has_anything_else_text (BooleanField) + cisa_representative_email (EmailField) + + cisa_representative_first_name (CharField) + + cisa_representative_last_name (CharField) + + has_cisa_representative (BooleanField) + is_policy_acknowledged (BooleanField) + notes (TextField) # other_contacts (ManyToManyField) @@ -224,6 +252,7 @@ class "registrar.DomainInformation " as registrar.DomainInformation # } registrar.DomainInformation -- registrar.FederalAgency registrar.DomainInformation -- registrar.User +registrar.DomainInformation -- registrar.Portfolio registrar.DomainInformation -- registrar.DomainRequest registrar.DomainInformation -- registrar.Contact registrar.DomainInformation -- registrar.Domain @@ -242,21 +271,6 @@ class "registrar.DraftDomain " as registrar.DraftDomain #d6f4e9 { } -class "registrar.UserDomainRole " 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 " as registrar.DomainInvitation #d6f4e9 { domain invitation -- @@ -388,6 +402,58 @@ class "registrar.WaffleFlag " as registrar.WaffleFlag #d6f4e9 { registrar.WaffleFlag *--* registrar.User +class "registrar.Portfolio " 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 " 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 " as registrar.Suborganization #d6f4e9 { + suborganization + -- + + id (BigAutoField) + + created_at (DateTimeField) + + updated_at (DateTimeField) + + name (CharField) + ~ portfolio (ForeignKey) + -- +} +registrar.Suborganization -- registrar.Portfolio + + @enduml ``` diff --git a/docs/architecture/diagrams/models_diagram.svg b/docs/architecture/diagrams/models_diagram.svg index 3fbc7dc10..f8cf3a46d 100644 --- a/docs/architecture/diagrams/models_diagram.svg +++ b/docs/architecture/diagrams/models_diagram.svg @@ -1 +1 @@ -registrarregistrar.ContactRegistrarcontactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (OneToOneField)first_name (CharField)middle_name (CharField)last_name (CharField)title (CharField)email (EmailField)phone (PhoneNumberField)registrar.UserRegistraruserid (BigAutoField)password (CharField)last_login (DateTimeField)is_superuser (BooleanField)username (CharField)first_name (CharField)last_name (CharField)email (EmailField)is_staff (BooleanField)is_active (BooleanField)date_joined (DateTimeField)status (CharField)phone (PhoneNumberField)middle_name (CharField)title (CharField)verification_type (CharField)groups (ManyToManyField)user_permissions (ManyToManyField)domains (ManyToManyField)registrar.HostRegistrarhostid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)domain (ForeignKey)registrar.DomainRegistrardomainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (DomainField)state (FSMField)expiration_date (DateField)security_contact_registry_id (TextField)deleted (DateField)first_ready (DateField)registrar.HostIPRegistrarhost ipid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)address (CharField)host (ForeignKey)registrar.PublicContactRegistrarpublic contactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)contact_type (CharField)registry_id (CharField)domain (ForeignKey)name (CharField)org (CharField)street1 (CharField)street2 (CharField)street3 (CharField)city (CharField)sp (CharField)pc (CharField)cc (CharField)email (EmailField)voice (CharField)fax (CharField)pw (CharField)registrar.FederalAgencyRegistrarFederal agencyid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)agency (CharField)registrar.DomainRequestRegistrardomain requestid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)status (FSMField)rejection_reason (TextField)federal_agency (ForeignKey)creator (ForeignKey)investigator (ForeignKey)generic_org_type (CharField)is_election_board (BooleanField)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (CharField)federal_type (CharField)organization_name (CharField)address_line1 (CharField)address_line2 (CharField)city (CharField)state_territory (CharField)zipcode (CharField)urbanization (CharField)about_your_organization (TextField)authorizing_official (ForeignKey)approved_domain (OneToOneField)requested_domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)has_anything_else_text (BooleanField)cisa_representative_email (EmailField)has_cisa_representative (BooleanField)is_policy_acknowledged (BooleanField)submission_date (DateField)notes (TextField)current_websites (ManyToManyField)alternative_domains (ManyToManyField)other_contacts (ManyToManyField)registrar.DraftDomainRegistrardraft domainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)registrar.WebsiteRegistrarwebsiteid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)website (CharField)registrar.DomainInformationRegistrardomain informationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)federal_agency (ForeignKey)creator (ForeignKey)domain_request (OneToOneField)generic_org_type (CharField)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (CharField)federal_type (CharField)is_election_board (BooleanField)organization_name (CharField)address_line1 (CharField)address_line2 (CharField)city (CharField)state_territory (CharField)zipcode (CharField)urbanization (CharField)about_your_organization (TextField)authorizing_official (ForeignKey)domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)cisa_representative_email (EmailField)is_policy_acknowledged (BooleanField)notes (TextField)other_contacts (ManyToManyField)registrar.UserDomainRoleRegistraruser domain roleid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (ForeignKey)domain (ForeignKey)role (TextField)registrar.DomainInvitationRegistrardomain invitationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)email (EmailField)domain (ForeignKey)status (FSMField)registrar.TransitionDomainRegistrartransition domainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)username (CharField)domain_name (CharField)status (CharField)email_sent (BooleanField)processed (BooleanField)generic_org_type (CharField)organization_name (CharField)federal_type (CharField)federal_agency (CharField)epp_creation_date (DateField)epp_expiration_date (DateField)first_name (CharField)middle_name (CharField)last_name (CharField)title (CharField)email (EmailField)phone (CharField)address_line (CharField)city (CharField)state_territory (CharField)zipcode (CharField)registrar.VerifiedByStaffRegistrarverified by staffid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)email (EmailField)requestor (ForeignKey)notes (TextField)registrar.UserGroupRegistrarUser groupid (AutoField)name (CharField)group_ptr (OneToOneField)permissions (ManyToManyField)registrar.WaffleFlagRegistrarwaffle flagid (BigAutoField)name (CharField)everyone (BooleanField)percent (DecimalField)testing (BooleanField)superusers (BooleanField)staff (BooleanField)authenticated (BooleanField)languages (TextField)rollout (BooleanField)note (TextField)created (DateTimeField)modified (DateTimeField)groups (ManyToManyField)users (ManyToManyField) \ No newline at end of file +registrarregistrar.ContactRegistrarcontactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (OneToOneField)first_name (CharField)middle_name (CharField)last_name (CharField)title (CharField)email (EmailField)phone (PhoneNumberField)registrar.UserRegistraruserid (BigAutoField)password (CharField)last_login (DateTimeField)is_superuser (BooleanField)username (CharField)first_name (CharField)last_name (CharField)email (EmailField)is_staff (BooleanField)is_active (BooleanField)date_joined (DateTimeField)status (CharField)phone (PhoneNumberField)middle_name (CharField)title (CharField)verification_type (CharField)groups (ManyToManyField)user_permissions (ManyToManyField)domains (ManyToManyField)registrar.HostRegistrarhostid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)domain (ForeignKey)registrar.DomainRegistrardomainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (DomainField)state (FSMField)expiration_date (DateField)security_contact_registry_id (TextField)deleted (DateField)first_ready (DateField)dsdata_last_change (TextField)registrar.HostIPRegistrarhost ipid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)address (CharField)host (ForeignKey)registrar.PublicContactRegistrarpublic contactid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)contact_type (CharField)registry_id (CharField)domain (ForeignKey)name (CharField)org (CharField)street1 (CharField)street2 (CharField)street3 (CharField)city (CharField)sp (CharField)pc (CharField)cc (CharField)email (EmailField)voice (CharField)fax (CharField)pw (CharField)registrar.UserDomainRoleRegistraruser domain roleid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)user (ForeignKey)domain (ForeignKey)role (TextField)registrar.FederalAgencyRegistrarFederal agencyid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)agency (CharField)federal_type (CharField)registrar.DomainRequestRegistrardomain requestid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)status (FSMField)rejection_reason (TextField)action_needed_reason (TextField)action_needed_reason_email (TextField)federal_agency (ForeignKey)portfolio (ForeignKey)creator (ForeignKey)investigator (ForeignKey)generic_org_type (CharField)is_election_board (BooleanField)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (CharField)federal_type (CharField)organization_name (CharField)address_line1 (CharField)address_line2 (CharField)city (CharField)state_territory (CharField)zipcode (CharField)urbanization (CharField)about_your_organization (TextField)senior_official (ForeignKey)approved_domain (OneToOneField)requested_domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)has_anything_else_text (BooleanField)cisa_representative_email (EmailField)cisa_representative_first_name (CharField)cisa_representative_last_name (CharField)has_cisa_representative (BooleanField)is_policy_acknowledged (BooleanField)submission_date (DateField)notes (TextField)current_websites (ManyToManyField)alternative_domains (ManyToManyField)other_contacts (ManyToManyField)registrar.PortfolioRegistrarportfolioid (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.DraftDomainRegistrardraft domainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)registrar.WebsiteRegistrarwebsiteid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)website (CharField)registrar.DomainInformationRegistrardomain informationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)federal_agency (ForeignKey)creator (ForeignKey)portfolio (ForeignKey)domain_request (OneToOneField)generic_org_type (CharField)organization_type (CharField)federally_recognized_tribe (BooleanField)state_recognized_tribe (BooleanField)tribe_name (CharField)federal_type (CharField)is_election_board (BooleanField)organization_name (CharField)address_line1 (CharField)address_line2 (CharField)city (CharField)state_territory (CharField)zipcode (CharField)urbanization (CharField)about_your_organization (TextField)senior_official (ForeignKey)domain (OneToOneField)submitter (ForeignKey)purpose (TextField)no_other_contacts_rationale (TextField)anything_else (TextField)has_anything_else_text (BooleanField)cisa_representative_email (EmailField)cisa_representative_first_name (CharField)cisa_representative_last_name (CharField)has_cisa_representative (BooleanField)is_policy_acknowledged (BooleanField)notes (TextField)other_contacts (ManyToManyField)registrar.DomainInvitationRegistrardomain invitationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)email (EmailField)domain (ForeignKey)status (FSMField)registrar.TransitionDomainRegistrartransition domainid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)username (CharField)domain_name (CharField)status (CharField)email_sent (BooleanField)processed (BooleanField)generic_org_type (CharField)organization_name (CharField)federal_type (CharField)federal_agency (CharField)epp_creation_date (DateField)epp_expiration_date (DateField)first_name (CharField)middle_name (CharField)last_name (CharField)title (CharField)email (EmailField)phone (CharField)address_line (CharField)city (CharField)state_territory (CharField)zipcode (CharField)registrar.VerifiedByStaffRegistrarverified by staffid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)email (EmailField)requestor (ForeignKey)notes (TextField)registrar.UserGroupRegistrarUser groupid (AutoField)name (CharField)group_ptr (OneToOneField)permissions (ManyToManyField)registrar.WaffleFlagRegistrarwaffle flagid (BigAutoField)name (CharField)everyone (BooleanField)percent (DecimalField)testing (BooleanField)superusers (BooleanField)staff (BooleanField)authenticated (BooleanField)languages (TextField)rollout (BooleanField)note (TextField)created (DateTimeField)modified (DateTimeField)groups (ManyToManyField)users (ManyToManyField)registrar.DomainGroupRegistrardomain groupid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)portfolio (ForeignKey)domains (ManyToManyField)registrar.SuborganizationRegistrarsuborganizationid (BigAutoField)created_at (DateTimeField)updated_at (DateTimeField)name (CharField)portfolio (ForeignKey) \ No newline at end of file diff --git a/src/.pa11yci b/src/.pa11yci index 56ea40416..c18704c07 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -11,7 +11,7 @@ "http://localhost:8080/request/org_federal/", "http://localhost:8080/request/org_election/", "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/dotgov_domain/", "http://localhost:8080/request/purpose/", diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 1a9064ac8..39282ff96 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -67,8 +67,8 @@ services: # command: "python" command: > bash -c " python manage.py migrate && - python manage.py load && python manage.py createcachetable && + python manage.py load && python manage.py runserver 0.0.0.0:8080" db: diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0500b692e..04730371c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -15,7 +15,6 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.urls import reverse -from dateutil.relativedelta import relativedelta # type: ignore from epplibwrapper.errors import ErrorCode, RegistryError from registrar.models.user_domain_role import UserDomainRole from waffle.admin import FlagAdmin @@ -449,7 +448,7 @@ class AdminSortFields: sort_mapping = { # == Contact == # "other_contacts": (Contact, _name_sort), - "authorizing_official": (Contact, _name_sort), + "senior_official": (Contact, _name_sort), "submitter": (Contact, _name_sort), # == User == # "creator": (User, _name_sort), @@ -603,6 +602,27 @@ class UserContactInline(admin.StackedInline): 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): """Custom user admin class to use our inlines.""" @@ -650,7 +670,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): None, {"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", { @@ -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", { @@ -705,7 +725,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): # NOT all fields are readonly for admin, otherwise we would have # set this at the permissions level. The exception is 'status' analyst_readonly_fields = [ - "Personal Info", + "User profile", "first_name", "middle_name", "last_name", @@ -942,6 +962,7 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Read only that we'll leverage for CISA Analysts analyst_readonly_fields = [ "user", + "email", ] def get_readonly_fields(self, request, obj=None): @@ -1238,9 +1259,9 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["portfolio", "creator", "submitter", "domain_request", "notes"]}), + (None, {"fields": ["portfolio", "sub_organization", "creator", "submitter", "domain_request", "notes"]}), (".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"]}), ( "Type of organization", @@ -1314,9 +1335,11 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): autocomplete_fields = [ "creator", "domain_request", - "authorizing_official", + "senior_official", "domain", "submitter", + "portfolio", + "sub_organization", ] # Table ordering @@ -1326,6 +1349,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): superuser_only_fields = [ "portfolio", + "sub_organization", ] # DEVELOPER's NOTE: @@ -1521,6 +1545,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): { "fields": [ "portfolio", + "sub_organization", "status_history", "status", "rejection_reason", @@ -1539,7 +1564,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "Contacts", { "fields": [ - "authorizing_official", + "senior_official", "other_contacts", "no_other_contacts_rationale", "cisa_representative_first_name", @@ -1630,13 +1655,16 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "requested_domain", "submitter", "creator", - "authorizing_official", + "senior_official", "investigator", + "portfolio", + "sub_organization", ] filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") superuser_only_fields = [ "portfolio", + "sub_organization", ] # DEVELOPER's NOTE: @@ -2056,14 +2084,7 @@ class DomainInformationInline(admin.StackedInline): fieldsets = DomainInformationAdmin.fieldsets readonly_fields = DomainInformationAdmin.readonly_fields analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields - - autocomplete_fields = [ - "creator", - "domain_request", - "authorizing_official", - "domain", - "submitter", - ] + autocomplete_fields = DomainInformationAdmin.autocomplete_fields def has_change_permission(self, request, obj=None): """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 - # in autocomplete_fields for domain + # this ordering effects the ordering of results in autocomplete_fields for domain ordering = ["name"] 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["domain_state"] = domain.get_state_display() - - # Pass in what the an extended expiration date would be for the expiration date modal - self._set_expiration_date_context(domain, extra_context) + extra_context["curr_exp_date"] = ( + domain.expiration_date if domain.expiration_date is not None else self._get_current_date() + ) 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): # Create dictionary of action functions ACTION_FUNCTIONS = { @@ -2304,11 +2311,9 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): self.message_user(request, "Object is not of type Domain.", messages.ERROR) return None - years = self._get_calculated_years_for_exp_date(obj) - # Renew the domain. try: - obj.renew_domain(length=years) + obj.renew_domain() self.message_user( request, "Successfully extended the expiration date.", @@ -2333,37 +2338,6 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): 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. # it is immutable. Rather than dealing with a convoluted workaround, # lets wrap this in a function. @@ -2706,6 +2680,11 @@ class PortfolioAdmin(ListHeaderAdmin): # readonly_fields = [ # "requestor", # ] + # Creates select2 fields (with search bars) + autocomplete_fields = [ + "creator", + "federal_agency", + ] def save_model(self, request, obj, form, change): @@ -2789,6 +2768,10 @@ class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): list_display = ["name", "portfolio"] + autocomplete_fields = [ + "portfolio", + ] + search_fields = ["name"] admin.site.unregister(LogEntry) # Unregister the default registration diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e6ae0927a..7052d786f 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -46,7 +46,7 @@ function ScrollToElement(attributeName, attributeValue) { } else if (attributeName === 'id') { targetEl = document.getElementById(attributeValue); } 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 } @@ -78,6 +78,50 @@ function makeVisible(el) { 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. */ function createLiveRegion(id) { const liveRegion = document.createElement("div"); @@ -927,7 +971,7 @@ function unloadModals() { * @param {string} itemName - The name displayed in the counter * @param {string} paginationSelector - CSS selector for the pagination container. * @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 {number} currentPage - The current page number (starting with 1). * @param {number} numPages - The total number of pages. @@ -936,7 +980,7 @@ function unloadModals() { * @param {number} totalItems - The total number of items. * @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 paginationCounter = document.querySelector(counterSelector); const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); @@ -955,7 +999,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA const prevPageItem = document.createElement('li'); prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; prevPageItem.innerHTML = ` - + @@ -974,7 +1018,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA const pageItem = document.createElement('li'); pageItem.className = 'usa-pagination__item usa-pagination__page-no'; pageItem.innerHTML = ` - ${page} + ${page} `; if (page === currentPage) { pageItem.querySelector('a').classList.add('usa-current'); @@ -1020,7 +1064,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA const nextPageItem = document.createElement('li'); nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; nextPageItem.innerHTML = ` - + Next

- This will extend the expiration date by one year. + This will extend the expiration date by one year from today. {# Acts as a
#}

This action cannot be undone. @@ -78,7 +78,7 @@ Domain: {{ original.name }} {# Acts as a
#}
- New expiration date: {{ extended_expiration_date }} + Current expiration date: {{ curr_exp_date }} {{test}}

diff --git a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html index 11141dca8..e29041168 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/contact_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/contact_description.html @@ -1,6 +1,6 @@

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 a corresponding record within the Users table.

diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index a46d49bc2..8b8748f80 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -168,10 +168,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %} - {% elif field.field.name == "authorizing_official" %} + {% elif field.field.name == "senior_official" %}
- - {% 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 %}
{% elif field.field.name == "other_contacts" and original_object.other_contacts.all %} {% with all_contacts=original_object.other_contacts.all %} diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 815df4942..06e0f4728 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -56,8 +56,8 @@ {% 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 %} - {% url 'domain-authorizing-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 %} + {% url 'domain-senior-official' pk=domain.id as url %} + {% 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 #} {% if not has_profile_feature_flag %} diff --git a/src/registrar/templates/domain_request_authorizing_official.html b/src/registrar/templates/domain_request_authorizing_official.html deleted file mode 100644 index df4931ba7..000000000 --- a/src/registrar/templates/domain_request_authorizing_official.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'domain_request_form.html' %} -{% load field_helpers url_helpers %} - -{% block form_instructions %} -

- Who is the authorizing official for your organization? -

- - {% if not is_federal %} -

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.

- {% endif %} - -
- {% include "includes/ao_example.html" %} -
- -

We typically don’t reach out to the authorizing official, but if contact is necessary, our practice is to coordinate with you, the requestor, first.

- -{% endblock %} - - -{% block form_fields %} -
- - Who is the authorizing official for your organization? - - - {% 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 %} - -
-{% endblock %} diff --git a/src/registrar/templates/domain_request_requirements.html b/src/registrar/templates/domain_request_requirements.html index c2581fad6..115c3ee69 100644 --- a/src/registrar/templates/domain_request_requirements.html +++ b/src/registrar/templates/domain_request_requirements.html @@ -40,7 +40,7 @@

We may need to suspend or terminate a domain registration for violations of these requirements. When we discover a violation, we’ll make reasonable efforts to contact a registrant, including emails or phone calls to:

  • Domain contacts
  • -
  • The authorizing official
  • +
  • The senior official
  • The government organization, a parent organization, or affiliated entities

diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index bb8ad498e..6776dd2c0 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -79,10 +79,10 @@ {% endwith %} {% endif %} - {% if step == Step.AUTHORIZING_OFFICIAL %} + {% if step == Step.SENIOR_OFFICIAL %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% if domain_request.authorizing_official is not None %} - {% with title=form_titles|get_item:step value=domain_request.authorizing_official %} + {% if domain_request.senior_official is not None %} + {% 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' %} {% endwith %} {% else %} diff --git a/src/registrar/templates/domain_request_senior_official.html b/src/registrar/templates/domain_request_senior_official.html new file mode 100644 index 000000000..74fbf49d2 --- /dev/null +++ b/src/registrar/templates/domain_request_senior_official.html @@ -0,0 +1,37 @@ +{% extends 'domain_request_form.html' %} +{% load field_helpers url_helpers %} + +{% block form_instructions %} +

+ Who is the senior official for your organization? +

+ + {% if not is_federal %} +

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.

+ {% endif %} + +
+ {% include "includes/so_example.html" %} +
+ +

We typically don’t reach out to the senior official, but if contact is necessary, our practice is to coordinate with you, the requestor, first.

+ +{% endblock %} + + +{% block form_fields %} +
+ + Who is the senior official for your organization? + + + {% 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 %} + +
+{% endblock %} diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index 16ce26f4c..ad3dc4069 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -86,8 +86,8 @@ {% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %} {% endif %} - {% if DomainRequest.authorizing_official %} - {% include "includes/summary_item.html" with title='Authorizing official' value=DomainRequest.authorizing_official contact='true' heading_level=heading_level %} + {% if DomainRequest.senior_official %} + {% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %} {% endif %} {% if DomainRequest.current_websites.all %} diff --git a/src/registrar/templates/domain_authorizing_official.html b/src/registrar/templates/domain_senior_official.html similarity index 72% rename from src/registrar/templates/domain_authorizing_official.html rename to src/registrar/templates/domain_senior_official.html index e5f2f09ba..5d13e28e7 100644 --- a/src/registrar/templates/domain_authorizing_official.html +++ b/src/registrar/templates/domain_senior_official.html @@ -1,20 +1,20 @@ {% extends "domain_base.html" %} {% load static field_helpers url_helpers %} -{% block title %}Authorizing official | {{ domain.name }} | {% endblock %} +{% block title %}Senior official | {{ domain.name }} | {% endblock %} {% block domain_content %} {# this is right after the messages block in the parent template #} {% include "includes/form_errors.html" with form=form %} -

Authorizing official

+

Senior official

-

Your authorizing 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 who can serve as an authorizing official.

+

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 who can serve as a senior official.

{% if generic_org_type == "federal" or generic_org_type == "tribal" %}

- The authorizing official for your organization can’t be updated here. + The senior official for your organization can’t be updated here. To suggest an update, email help@get.gov.

{% else %} @@ -27,10 +27,10 @@ {% if generic_org_type == "federal" or generic_org_type == "tribal" %} {# If all fields are disabled, add SR content #} -
{{ form.first_name.value }}
-
{{ form.last_name.value }}
-
{{ form.title.value }}
-
{{ form.email.value }}
+
{{ form.first_name.value }}
+
{{ form.last_name.value }}
+
{{ form.title.value }}
+
{{ form.email.value }}
{% endif %} {% input_with_errors form.first_name %} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 5739d0417..d61e5f45c 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -65,11 +65,11 @@
  • - {% url 'domain-authorizing-official' pk=domain.id as url %} + {% url 'domain-senior-official' pk=domain.id as url %} - Authorizing official + Senior official
  • diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 5196b641a..8e4f04fcd 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -8,7 +8,7 @@

    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.

    diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt similarity index 52% rename from src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt rename to src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt index 259968204..94e15b78c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt @@ -9,18 +9,18 @@ STATUS: Action needed ---------------------------------------------------------------- -AUTHORIZING 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: -- {{ domain_request.authorizing_official.get_formatted_name }} -- {{ domain_request.authorizing_official.title }} +SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS +We've reviewed your domain request, but we need more information about the senior official listed on the request: +- {{ domain_request.senior_official.get_formatted_name }} +- {{ 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. +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. 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. Once you submit your updated request, we’ll resume the adjudication process. +Alternatively, you can log in to the registrar and enter a different senior official for this domain request. Once you submit your updated request, we’ll resume the adjudication process. THANK YOU diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt similarity index 100% rename from src/registrar/templates/emails/action_needed_reasons/questionable_authorizing_official_subject.txt rename to src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt diff --git a/src/registrar/templates/emails/includes/domain_request_summary.txt b/src/registrar/templates/emails/includes/domain_request_summary.txt index b3f6c0d95..9294420c4 100644 --- a/src/registrar/templates/emails/includes/domain_request_summary.txt +++ b/src/registrar/templates/emails/includes/domain_request_summary.txt @@ -27,8 +27,8 @@ Organization name and mailing address: About your organization: {% spaceless %}{{ domain_request.about_your_organization }}{% endspaceless %} {% endif %} -Authorizing official: -{% spaceless %}{% include "emails/includes/contact.txt" with contact=domain_request.authorizing_official %}{% endspaceless %} +Senior official: +{% spaceless %}{% include "emails/includes/contact.txt" with contact=domain_request.senior_official %}{% endspaceless %} {% if domain_request.current_websites.exists %}{# if block makes a newline #} Current websites: {% for site in domain_request.current_websites.all %} {% spaceless %}{{ site.website }}{% endspaceless %} diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index e760687b6..4f091ecf6 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -1,6 +1,6 @@ {% load static %} -
    +
    {% if portfolio is None %}
    @@ -11,10 +11,10 @@
    -