diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 47fb2c226..254078246 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -31,8 +31,8 @@ body: attributes: label: Links to other issues description: | - "Add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)." - placeholder: 🔄 Relates to... + "With a `-` to start the line, add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)." + placeholder: "- 🔄 Relates to..." - type: markdown id: note attributes: diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 325fc53dd..049eb38b4 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -9,6 +9,7 @@ from django.db.models.functions import Concat, Coalesce from django.http import HttpResponseRedirect from django.shortcuts import redirect from django_fsm import get_available_FIELD_transitions, FSMField +from waffle.decorators import flag_is_active from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import Group @@ -18,7 +19,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError from registrar.models.user_domain_role import UserDomainRole from waffle.admin import FlagAdmin from waffle.models import Sample, Switch -from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website +from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.views.utility.mixins import OrderableFieldsMixin from django.contrib.admin.views.main import ORDER_VAR @@ -166,6 +167,9 @@ class DomainRequestAdminForm(forms.ModelForm): "alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False), "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False), } + labels = { + "action_needed_reason_email": "Auto-generated email", + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -444,8 +448,9 @@ class AdminSortFields: sort_mapping = { # == Contact == # "other_contacts": (Contact, _name_sort), - "senior_official": (Contact, _name_sort), "submitter": (Contact, _name_sort), + # == Senior Official == # + "senior_official": (SeniorOfficial, _name_sort), # == User == # "creator": (User, _name_sort), "user": (User, _name_sort), @@ -997,6 +1002,19 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().changelist_view(request, extra_context=extra_context) +class SeniorOfficialAdmin(ListHeaderAdmin): + """Custom Senior Official Admin class.""" + + # NOTE: these are just placeholders. Not part of ACs (haven't been defined yet). Update in future tickets. + search_fields = ["first_name", "last_name", "email"] + search_help_text = "Search by first name, last name or email." + list_display = ["first_name", "last_name", "email"] + + # this ordering effects the ordering of results + # in autocomplete_fields for Senior Official + ordering = ["first_name", "last_name"] + + class WebsiteResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file""" @@ -1467,6 +1485,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): custom_election_board.admin_order_field = "is_election_board" # type: ignore custom_election_board.short_description = "Election office" # type: ignore + # This is just a placeholder. This field will be populated in the detail_table_fieldset view. + # This is not a field that exists on the model. + def status_history(self, obj): + return "No changelog to display." + + status_history.short_description = "Status History" # type: ignore + # Filters list_filter = ( StatusListFilter, @@ -1493,9 +1518,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "fields": [ "portfolio", "sub_organization", + "status_history", "status", "rejection_reason", "action_needed_reason", + "action_needed_reason_email", "investigator", "creator", "submitter", @@ -1575,6 +1602,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "alternative_domains", "is_election_board", "federal_agency", + "status_history", + "action_needed_reason_email", ) # Read only that we'll leverage for CISA Analysts @@ -1893,6 +1922,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): extra_context = extra_context or {} extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries extra_context["action_needed_reason_emails"] = self.get_all_action_needed_reason_emails_as_json(obj) + extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") # Call the superclass method with updated extra_context return super().change_view(request, object_id, form_url, extra_context) @@ -1923,9 +1953,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt" subject_template = get_template(template_subject_path) - # Return the content of the rendered views - context = {"domain_request": domain_request} + if flag_is_active(None, "profile_feature"): # type: ignore + recipient = domain_request.creator + else: + recipient = domain_request.submitter + # Return the content of the rendered views + context = {"domain_request": domain_request, "recipient": recipient} return { "subject_text": subject_template.render(context=context), "email_body_text": template.render(context=context), @@ -2737,6 +2771,7 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) admin.site.register(models.Portfolio, PortfolioAdmin) admin.site.register(models.DomainGroup, DomainGroupAdmin) admin.site.register(models.Suborganization, SuborganizationAdmin) +admin.site.register(models.SeniorOfficial, SeniorOfficialAdmin) # Register our custom waffle implementations admin.site.register(models.WaffleFlag, WaffleFlagAdmin) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 94fdd8b07..ad759f445 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -361,9 +361,12 @@ function initializeWidgetOnList(list, parentId) { */ (function (){ let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') + // This is the "action needed reason" field let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason'); + // This is the "auto-generated email" field + let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email') - if (rejectionReasonFormGroup && actionNeededReasonFormGroup) { + if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) { let statusSelect = document.getElementById('id_status') let isRejected = statusSelect.value == "rejected" let isActionNeeded = statusSelect.value == "action needed" @@ -371,6 +374,7 @@ function initializeWidgetOnList(list, parentId) { // Initial handling of rejectionReasonFormGroup display showOrHideObject(rejectionReasonFormGroup, show=isRejected) showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded) + showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded) // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage statusSelect.addEventListener('change', function() { @@ -382,6 +386,7 @@ function initializeWidgetOnList(list, parentId) { isActionNeeded = statusSelect.value == "action needed" showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded) + showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded) addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded) }); @@ -398,6 +403,7 @@ function initializeWidgetOnList(list, parentId) { let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason) + showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded) } }); }); @@ -421,42 +427,6 @@ function initializeWidgetOnList(list, parentId) { sessionStorage.removeItem(name); } } - - document.addEventListener('DOMContentLoaded', function() { - let statusSelect = document.getElementById('id_status'); - - function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) { - if (!actionNeededReasonFormGroup || !statusSelect) { - return; - } - - let flexContainer = actionNeededReasonFormGroup.querySelector('.flex-container'); - let statusChangelog = document.getElementById('dja-status-changelog'); - - // On action needed, show the email that will be sent out - let showReasonEmailContainer = document.querySelector("#action_needed_reason_email_readonly") - - // Prepopulate values on page load. - if (statusSelect.value === "action needed") { - flexContainer.parentNode.insertBefore(statusChangelog, flexContainer.nextSibling); - showElement(showReasonEmailContainer); - } else { - // Move the changelog back to its original location - let statusFlexContainer = statusSelect.closest('.flex-container'); - statusFlexContainer.parentNode.insertBefore(statusChangelog, statusFlexContainer.nextSibling); - hideElement(showReasonEmailContainer); - } - - } - - // Call the function on page load - moveStatusChangelog(actionNeededReasonFormGroup, statusSelect); - - // Add event listener to handle changes to the selector itself - statusSelect.addEventListener('change', function() { - moveStatusChangelog(actionNeededReasonFormGroup, statusSelect); - }) - }); })(); /** An IIFE for toggling the submit bar on domain request forms @@ -552,13 +522,13 @@ function initializeWidgetOnList(list, parentId) { })(); -/** An IIFE that hooks up to the "show email" button. - * which shows the auto generated email on action needed reason. +/** An IIFE that hooks to the show/hide button underneath action needed reason. + * This shows the auto generated email on action needed reason. */ (function () { let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason"); let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more"); - if(actionNeededReasonDropdown && actionNeededEmail && container) { + if(actionNeededReasonDropdown && actionNeededEmail) { // Add a change listener to the action needed reason dropdown handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail); } diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index aa8020ede..d7d116046 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -787,10 +787,16 @@ div.dja__model-description{ color: var(--link-fg); } +.textarea-wrapper { + width: 100%; + max-width: 610px; +} + .dja-readonly-textarea-container { + width: 100%; textarea { width: 100%; - min-width: 610px; + max-width: 610px; resize: none; cursor: auto; @@ -827,3 +833,20 @@ div.dja__model-description{ // Many elements in django admin try to override this, so we need !important. display: none !important; } + +.margin-top-0 { + margin-top: 0 !important; +} + +.padding-top-0 { + padding-top: 0 !important; +} + + +.flex-container { + @media screen and (min-width: 700px) and (max-width: 1150px) { + &.flex-container--mobile-inline { + display: inline !important; + } + } +} diff --git a/src/registrar/migrations/0110_seniorofficial_portfolio_senior_official.py b/src/registrar/migrations/0110_seniorofficial_portfolio_senior_official.py new file mode 100644 index 000000000..c344898c3 --- /dev/null +++ b/src/registrar/migrations/0110_seniorofficial_portfolio_senior_official.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.10 on 2024-07-02 21:03 + +from django.db import migrations, models +import django.db.models.deletion +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0109_domaininformation_sub_organization_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="SeniorOfficial", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("first_name", models.CharField(verbose_name="first name")), + ("last_name", models.CharField(verbose_name="last name")), + ("title", models.CharField(verbose_name="title / role")), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), + ), + ("email", models.EmailField(blank=True, max_length=320, null=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="portfolio", + name="senior_official", + field=models.ForeignKey( + blank=True, + help_text="Associated senior official", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="registrar.seniorofficial", + ), + ), + ] diff --git a/src/registrar/migrations/0111_create_groups_v15.py b/src/registrar/migrations/0111_create_groups_v15.py new file mode 100644 index 000000000..6b21f4b0d --- /dev/null +++ b/src/registrar/migrations/0111_create_groups_v15.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0079 (which populates federal agencies) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0110_seniorofficial_portfolio_senior_official"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 376739826..a68633aff 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -19,6 +19,7 @@ from .waffle_flag import WaffleFlag from .portfolio import Portfolio from .domain_group import DomainGroup from .suborganization import Suborganization +from .senior_official import SeniorOfficial __all__ = [ @@ -42,6 +43,7 @@ __all__ = [ "Portfolio", "DomainGroup", "Suborganization", + "SeniorOfficial", ] auditlog.register(Contact) @@ -64,3 +66,4 @@ auditlog.register(WaffleFlag) auditlog.register(Portfolio) auditlog.register(DomainGroup) auditlog.register(Suborganization) +auditlog.register(SeniorOfficial) diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 0ea036bb7..c72f95c33 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -38,6 +38,15 @@ class Portfolio(TimeStampedModel): default=FederalAgency.get_non_federal_agency, ) + senior_official = models.ForeignKey( + "registrar.SeniorOfficial", + on_delete=models.PROTECT, + help_text="Associated senior official", + unique=False, + null=True, + blank=True, + ) + organization_type = models.CharField( max_length=255, choices=OrganizationChoices.choices, diff --git a/src/registrar/models/senior_official.py b/src/registrar/models/senior_official.py new file mode 100644 index 000000000..3cb064790 --- /dev/null +++ b/src/registrar/models/senior_official.py @@ -0,0 +1,50 @@ +from django.db import models + +from .utility.time_stamped_model import TimeStampedModel +from phonenumber_field.modelfields import PhoneNumberField # type: ignore + + +class SeniorOfficial(TimeStampedModel): + """ + Senior Official is a distinct Contact-like entity (NOT to be inherited + from Contacts) developed for the unique role these individuals have in + managing Portfolios. + """ + + first_name = models.CharField( + null=False, + blank=False, + verbose_name="first name", + ) + last_name = models.CharField( + null=False, + blank=False, + verbose_name="last name", + ) + title = models.CharField( + null=False, + blank=False, + verbose_name="title / role", + ) + phone = PhoneNumberField( + null=True, + blank=True, + ) + email = models.EmailField( + null=True, + blank=True, + max_length=320, + ) + + def get_formatted_name(self): + """Returns the contact's name in Western order.""" + names = [n for n in [self.first_name, self.last_name] if n] + return " ".join(names) if names else "Unknown" + + def __str__(self): + if self.first_name or self.last_name: + return self.get_formatted_name() + elif self.pk: + return str(self.pk) + else: + return "" diff --git a/src/registrar/templates/admin/fieldset.html b/src/registrar/templates/admin/fieldset.html index 89537b098..40cd98ca8 100644 --- a/src/registrar/templates/admin/fieldset.html +++ b/src/registrar/templates/admin/fieldset.html @@ -32,7 +32,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ {% for field in line %}
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %} + {% block flex_container_start %}
+ {% endblock flex_container_start %} {% if field.is_checkbox %} {# .gov override #} {% block field_checkbox %} @@ -52,7 +54,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ {% endblock field_other%} {% endif %} {% endif %} + {% block flex_container_end %}
+ {% endblock flex_container_end %} {% if field.field.help_text %} {# .gov override #} 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 e3e7ef42c..8b8748f80 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -6,9 +6,85 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endcomment %} +{% block flex_container_start %} + {% if field.field.name == "status_history" %} +
+ {% else %} + {% comment %} Default flex container element {% endcomment %} +
+ {% endif %} +{% endblock flex_container_start %} + {% block field_readonly %} {% with all_contacts=original_object.other_contacts.all %} - {% if field.field.name == "other_contacts" %} + {% if field.field.name == "status_history" %} + {% if filtered_audit_log_entries %} +
+ + +
+ {% else %} +
+ No changelog to display. +
+ {% endif %} + {% elif field.field.name == "action_needed_reason_email" %} +
+ + +
+ {% elif field.field.name == "other_contacts" %} {% if all_contacts.count > 2 %}
{% for contact in all_contacts %} @@ -68,80 +144,19 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endblock field_readonly %} {% block after_help_text %} - {% if field.field.name == "status" %} -
- -
- - - - -
-
+ Given that we have a limited number of emails, doing it this way makes sense. + {% endcomment %} + {% if action_needed_reason_emails %} + + {% endif %} {% elif field.field.name == "creator" %}
diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 370ea2b2b..f4319f53d 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -18,7 +18,7 @@ completing your domain request might take around 15 minutes.

{% if has_profile_feature_flag %}

How we’ll reach you

-

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit your profile to make updates.

+

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review. If the contact information below is not correct, visit your profile to make updates.

{% include "includes/profile_information.html" with user=user%} {% endif %} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index c8fe64c42..4fa443105 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -44,6 +44,7 @@ from registrar.models import ( UserGroup, TransitionDomain, ) +from registrar.models.senior_official import SeniorOfficial from registrar.models.user_domain_role import UserDomainRole from registrar.models.verified_by_staff import VerifiedByStaff from .common import ( @@ -931,6 +932,32 @@ class TestDomainRequestAdmin(MockEppLib): ) self.mock_client = MockSESClient() + def test_domain_request_senior_official_is_alphabetically_sorted(self): + """Tests if the senior offical dropdown is alphanetically sorted in the django admin display""" + + SeniorOfficial.objects.get_or_create(first_name="mary", last_name="joe", title="some other guy") + SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy") + SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title") + + contact, _ = Contact.objects.get_or_create(user=self.staffuser) + domain_request = completed_domain_request(submitter=contact, name="city1.gov") + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + model_admin = AuditedAdmin(DomainRequest, self.site) + + # Get the queryset that would be returned for the list + senior_offical_queryset = model_admin.formfield_for_foreignkey( + DomainInformation.senior_official.field, request + ).queryset + + # Make the list we're comparing on a bit prettier display-wise. Optional step. + current_sort_order = [] + for official in senior_offical_queryset: + current_sort_order.append(f"{official.first_name} {official.last_name}") + + expected_sort_order = ["alex smoe", "mary joe", "Zoup Soup"] + + self.assertEqual(current_sort_order, expected_sort_order) + @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" @@ -2233,6 +2260,8 @@ class TestDomainRequestAdmin(MockEppLib): "alternative_domains", "is_election_board", "federal_agency", + "status_history", + "action_needed_reason_email", "id", "created_at", "updated_at", @@ -2293,6 +2322,8 @@ class TestDomainRequestAdmin(MockEppLib): "alternative_domains", "is_election_board", "federal_agency", + "status_history", + "action_needed_reason_email", "creator", "about_your_organization", "requested_domain", @@ -2323,6 +2354,8 @@ class TestDomainRequestAdmin(MockEppLib): "alternative_domains", "is_election_board", "federal_agency", + "status_history", + "action_needed_reason_email", ] self.assertEqual(readonly_fields, expected_fields) @@ -2719,6 +2752,7 @@ class TestDomainRequestAdmin(MockEppLib): User.objects.all().delete() Contact.objects.all().delete() Website.objects.all().delete() + SeniorOfficial.objects.all().delete() self.mock_client.EMAILS_SENT.clear() @@ -2900,6 +2934,38 @@ class TestDomainInformationAdmin(TestCase): Domain.objects.all().delete() Contact.objects.all().delete() User.objects.all().delete() + SeniorOfficial.objects.all().delete() + + def test_domain_information_senior_official_is_alphabetically_sorted(self): + """Tests if the senior offical dropdown is alphanetically sorted in the django admin display""" + + SeniorOfficial.objects.get_or_create(first_name="mary", last_name="joe", title="some other guy") + SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy") + SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title") + + contact, _ = Contact.objects.get_or_create(user=self.staffuser) + domain_request = completed_domain_request( + submitter=contact, name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW + ) + domain_request.approve() + + domain_info = DomainInformation.objects.get(domain_request=domain_request) + request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(domain_info.pk)) + model_admin = AuditedAdmin(DomainInformation, self.site) + + # Get the queryset that would be returned for the list + senior_offical_queryset = model_admin.formfield_for_foreignkey( + DomainInformation.senior_official.field, request + ).queryset + + # Make the list we're comparing on a bit prettier display-wise. Optional step. + current_sort_order = [] + for official in senior_offical_queryset: + current_sort_order.append(f"{official.first_name} {official.last_name}") + + expected_sort_order = ["alex smoe", "mary joe", "Zoup Soup"] + + self.assertEqual(current_sort_order, expected_sort_order) @less_console_noise_decorator def test_admin_can_see_cisa_region_federal(self): @@ -3650,6 +3716,7 @@ class AuditedAdminTest(TestCase): self.site = AdminSite() self.factory = RequestFactory() self.client = Client(HTTP_HOST="localhost:8080") + self.staffuser = create_user() def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names): with less_console_noise(): @@ -3701,7 +3768,9 @@ class AuditedAdminTest(TestCase): def test_alphabetically_sorted_fk_fields_domain_request(self): with less_console_noise(): tested_fields = [ - DomainRequest.senior_official.field, + # Senior offical is commented out for now - this is alphabetized + # and this test does not accurately reflect that. + # DomainRequest.senior_official.field, DomainRequest.submitter.field, # DomainRequest.investigator.field, DomainRequest.creator.field, @@ -3759,7 +3828,9 @@ class AuditedAdminTest(TestCase): def test_alphabetically_sorted_fk_fields_domain_information(self): with less_console_noise(): tested_fields = [ - DomainInformation.senior_official.field, + # Senior offical is commented out for now - this is alphabetized + # and this test does not accurately reflect that. + # DomainInformation.senior_official.field, DomainInformation.submitter.field, # DomainInformation.creator.field, (DomainInformation.domain.field, ["name"]), @@ -3792,7 +3863,6 @@ class AuditedAdminTest(TestCase): # Conforms to the same object structure as desired_order current_sort_order_coerced_type = [] - # This is necessary as .queryset and get_queryset # return lists of different types/structures. # We need to parse this data and coerce them into the same type. @@ -3869,7 +3939,8 @@ class AuditedAdminTest(TestCase): if last_name is None: return (first_name,) - if first_name.split(queryset_shorthand)[1] == field_name: + split_name = first_name.split(queryset_shorthand) + if len(split_name) == 2 and split_name[1] == field_name: return returned_tuple else: return None diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index e24ccfaf8..7ff054ef6 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -601,6 +601,7 @@ class FinishUserProfileTests(TestWithUser, WebTest): # Add a phone number finish_setup_form = finish_setup_page.form + finish_setup_form["first_name"] = "firstname" finish_setup_form["phone"] = "(201) 555-0123" finish_setup_form["title"] = "CEO" finish_setup_form["last_name"] = "example" @@ -729,6 +730,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest): self.assertContains(save_page, "Your profile has been updated.") # We need to assert that logo is not clickable and links to manage your domain are not present + # NOTE: "anage" is not a typo. It is to accomodate the fact that the "m" is uppercase in one + # instance and lowercase in the other. self.assertContains(save_page, "anage your domains", count=2) self.assertNotContains( save_page, "Before you can manage your domains, we need you to add contact information" diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index dec1d1af1..20a4e3324 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -1,5 +1,4 @@ """Views for a User Profile. - """ import logging @@ -106,6 +105,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin): """If the form is invalid, conditionally display an additional error.""" if hasattr(self.user, "finished_setup") and not self.user.finished_setup: messages.error(self.request, "Before you can manage your domain, we need you to add contact information.") + form.initial["redirect"] = form.data.get("redirect") return super().form_invalid(form) def form_valid(self, form):