diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3b9872e48..d8ef268ca 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 registrar.models.domain_information import DomainInformation from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from waffle.decorators import flag_is_active from django.contrib import admin, messages @@ -3142,12 +3143,32 @@ class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): + list_display = ["name", "portfolio"] autocomplete_fields = [ "portfolio", ] search_fields = ["name"] + change_form_template = "django/admin/suborg_change_form.html" + + def change_view(self, request, object_id, form_url="", extra_context=None): + """Add suborg's related domains and requests to context""" + obj = self.get_object(request, object_id) + + # ---- Domain Requests + domain_requests = DomainRequest.objects.filter(sub_organization=obj) + sort_by = request.GET.get("sort_by", "requested_domain__name") + domain_requests = domain_requests.order_by(sort_by) + + # ---- Domains + domain_infos = DomainInformation.objects.filter(sub_organization=obj) + domain_ids = domain_infos.values_list("domain", flat=True) + domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED) + + extra_context = {"domain_requests": domain_requests, "domains": domains} + return super().change_view(request, object_id, form_url, extra_context) + admin.site.unregister(LogEntry) # Unregister the default registration diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index 7ce63d364..0fc203248 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -22,6 +22,12 @@ class UserFixture: """ ADMINS = [ + { + "username": "43a7fa8d-0550-4494-a6fe-81500324d590", + "first_name": "Jyoti", + "last_name": "Bock", + "email": "jyotibock@truss.works", + }, { "username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf", "first_name": "Aditi", @@ -125,6 +131,12 @@ class UserFixture: ] STAFF = [ + { + "username": "a5906815-dd80-4c64-aebe-2da6a4c9d7a4", + "first_name": "Jyoti-Analyst", + "last_name": "Bock-Analyst", + "email": "jyotibock+1@truss.works", + }, { "username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54", "first_name": "Aditi-Analyst", diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 431aa30a7..d97dd0de7 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -679,7 +679,7 @@ class CisaRepresentativeYesNoForm(BaseYesNoForm): field_name = "has_cisa_representative" -class AdditionalDetailsForm(BaseDeletableRegistrarForm): +class AnythingElseForm(BaseDeletableRegistrarForm): anything_else = forms.CharField( required=True, label="Anything else?", @@ -698,7 +698,7 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm): ) -class AdditionalDetailsYesNoForm(BaseYesNoForm): +class AnythingElseYesNoForm(BaseYesNoForm): """Yes/no toggle for the anything else question on additional details""" # Note that these can be set as functions/init if you need more fine-grained control. diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index bdd67e582..774dba897 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -296,23 +296,29 @@ class DomainInformation(TimeStampedModel): """Some yes/no forms use a db field to track whether it was checked or not. We handle that here for def save(). """ + # Check if the firstname or lastname of cisa representative has any data. + # Then set the has_cisa_representative flag accordingly (so that it isn't + # "none", which indicates an incomplete form). # This ensures that if we have prefilled data, the form is prepopulated if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None: self.has_cisa_representative = ( self.cisa_representative_first_name != "" and self.cisa_representative_last_name != "" ) - # This check is required to ensure that the form doesn't start out checked + # Check for blank data and update has_cisa_representative accordingly (if it isn't None) if self.has_cisa_representative is not None: self.has_cisa_representative = ( self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None ) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None) + # Check if anything_else has any data. + # Then set the has_anything_else_text flag accordingly (so that it isn't + # "none", which indicates an incomplete form). # This ensures that if we have prefilled data, the form is prepopulated if self.anything_else is not None: self.has_anything_else_text = self.anything_else != "" - # This check is required to ensure that the form doesn't start out checked. + # Check for blank data and update has_anything_else_text accordingly (if it isn't None) if self.has_anything_else_text is not None: self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 363de213b..966c880d7 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -645,23 +645,29 @@ class DomainRequest(TimeStampedModel): """Some yes/no forms use a db field to track whether it was checked or not. We handle that here for def save(). """ + # Check if the firstname or lastname of cisa representative has any data. + # Then set the has_cisa_representative flag accordingly (so that it isn't + # "none", which indicates an incomplete form). # This ensures that if we have prefilled data, the form is prepopulated if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None: self.has_cisa_representative = ( self.cisa_representative_first_name != "" and self.cisa_representative_last_name != "" ) - # This check is required to ensure that the form doesn't start out checked + # Check for blank data and update has_cisa_representative accordingly (if it isn't None) if self.has_cisa_representative is not None: self.has_cisa_representative = ( self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None ) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None) + # Check if anything_else has any data. + # Then set the has_anything_else_text flag accordingly (so that it isn't + # "none", which indicates an incomplete form). # This ensures that if we have prefilled data, the form is prepopulated if self.anything_else is not None: self.has_anything_else_text = self.anything_else != "" - # This check is required to ensure that the form doesn't start out checked. + # Check for blank data and update has_anything_else_text accordingly (if it isn't None) if self.has_anything_else_text is not None: self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None diff --git a/src/registrar/templates/django/admin/suborg_change_form.html b/src/registrar/templates/django/admin/suborg_change_form.html new file mode 100644 index 000000000..005d67aec --- /dev/null +++ b/src/registrar/templates/django/admin/suborg_change_form.html @@ -0,0 +1,36 @@ +{% extends 'django/admin/email_clipboard_change_form.html' %} +{% load i18n static %} + +{% block after_related_objects %} +
+

Associated requests and domains

+
+
+

Domain requests

+ +
+
+

Domains

+ +
+
+
+{% endblock %} diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index 05ce46114..a8d85597b 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import ( RequirementsForm, TribalGovernmentForm, PurposeForm, - AdditionalDetailsForm, + AnythingElseForm, AboutYourOrganizationForm, ) from registrar.forms.domain import ContactForm @@ -274,7 +274,7 @@ class TestFormValidation(MockEppLib): def test_anything_else_form_about_your_organization_character_count_invalid(self): """Response must be less than 2000 characters.""" - form = AdditionalDetailsForm( + form = AnythingElseForm( data={ "anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami" "shankle, drumstick doner chicken landjaeger turkey andouille." diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 0cee9d563..6642b6471 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -1017,20 +1017,27 @@ class DomainRequestTests(TestWithUser, WebTest): type_page = intro_result.follow() session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # fill out the organization type section then submit type_form = type_page.forms[0] type_form["generic_org_type-generic_org_type"] = "federal" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) type_result = type_form.submit() - # follow first redirect + # follow first redirect to the next section self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) federal_page = type_result.follow() - # Now on federal type page, click back to the organization type + # we need to fill out the federal section so it stays unlocked + fed_branch_form = federal_page.forms[0] + fed_branch_form["organization_federal-federal_type"] = "executive" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + fed_branch_form.submit() + + # Now click back to the organization type self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) new_page = federal_page.click(str(self.TITLES["generic_org_type"]), index=0) - # Should be a link to the organization_federal page + # Should be a link to the organization_federal page since it is now unlocked self.assertGreater( len(new_page.html.find_all("a", href="/request/organization_federal/")), 0, @@ -2528,9 +2535,22 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) election_page = type_result.follow() - # Go back to SO page and test the dynamic text changed + # Navigate to the org page as that is the step right before senior_official + org_page = election_page.click(str(self.TITLES["organization_contact"]), index=0) + org_contact_form = org_page.forms[0] + org_contact_form["organization_contact-organization_name"] = "Testorg" + org_contact_form["organization_contact-address_line1"] = "address 1" + org_contact_form["organization_contact-address_line2"] = "address 2" + org_contact_form["organization_contact-city"] = "NYC" + org_contact_form["organization_contact-state_territory"] = "NY" + org_contact_form["organization_contact-zipcode"] = "10002" + org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - so_page = election_page.click(str(self.TITLES["senior_official"]), index=0) + org_contact_result = org_contact_form.submit() + + # Navigate back to the so page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + so_page = org_contact_result.follow() self.assertContains(so_page, "Domain requests from cities") @less_console_noise_decorator @@ -2628,9 +2648,15 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) election_page = type_result.follow() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_websites = election_page.click(str(self.TITLES["current_sites"]), index=0) + current_sites_form = current_websites.forms[0] + current_sites_form["current_sites-0-website"] = "www.city.com" + current_sites_result = current_sites_form.submit().follow() + # Go back to dotgov domain page to test the dynamic text changed self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page = election_page.click(str(self.TITLES["dotgov_domain"]), index=0) + dotgov_page = current_sites_result.click(str(self.TITLES["dotgov_domain"]), index=0) self.assertContains(dotgov_page, "CityofEudoraKS.gov") self.assertNotContains(dotgov_page, "medicare.gov") @@ -2984,6 +3010,9 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): """Test when all fields in the domain request are filled.""" domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user) + domain_request.anything_else = False + domain_request.has_anything_else_text = False + domain_request.save() response = self.app.get(f"/domain-request/{domain_request.id}/edit/") # django-webtest does not handle cookie-based sessions well because it keeps diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 08e23e402..b691549cd 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -217,7 +217,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): if current_url == self.EDIT_URL_NAME and "id" in kwargs: del self.storage self.storage["domain_request_id"] = kwargs["id"] - self.storage["step_history"] = self.db_check_for_unlocking_steps() # if accessing this class directly, redirect to either to an acknowledgement # page or to the first step in the processes (if an edit rather than a new request); @@ -233,6 +232,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): else: return self.goto(self.steps.first) + # refresh step_history to ensure we don't erroneously unlock unfinished + # steps just because we visited it + self.storage["step_history"] = self.db_check_for_unlocking_steps() context = self.get_context_data() self.steps.current = current_url context["forms"] = self.get_forms() @@ -341,6 +343,17 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): """Helper for get_context_data Queries the DB for a domain request and returns a list of unlocked steps.""" + + # The way this works is as follows: + # Each step is assigned a true/false value to determine if it is + # "unlocked" or not. This dictionary of values is looped through + # at the end of this function and any step with a "true" value is + # added to a simple array that is returned at the end of this function. + # This array is eventually passed to the frontend context (eg. domain_request_sidebar.html), + # and is used to determine how steps appear in the side nav. + # It is worth noting that any step assigned "false" here will be EXCLUDED + # from the list of "unlocked" steps. + history_dict = { "generic_org_type": self.domain_request.generic_org_type is not None, "tribal_government": self.domain_request.tribe_name is not None, @@ -368,8 +381,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): or self.domain_request.no_other_contacts_rationale is not None ), "additional_details": ( - (self.domain_request.anything_else is not None and self.domain_request.has_cisa_representative) - or self.domain_request.is_policy_acknowledged is not None + # Additional details is complete as long as "has anything else" and "has cisa rep" are not None + ( + self.domain_request.has_anything_else_text is not None + and self.domain_request.has_cisa_representative is not None + ) ), "requirements": self.domain_request.is_policy_acknowledged is not None, "review": self.domain_request.is_policy_acknowledged is not None, @@ -626,8 +642,8 @@ class AdditionalDetails(DomainRequestWizard): forms = [ forms.CisaRepresentativeYesNoForm, forms.CisaRepresentativeForm, - forms.AdditionalDetailsYesNoForm, - forms.AdditionalDetailsForm, + forms.AnythingElseYesNoForm, + forms.AnythingElseForm, ] def is_valid(self, forms: list) -> bool: