diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 89f7522b3..d7a02b124 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -2,7 +2,8 @@
import logging
from api.views import DOMAIN_API_MESSAGES
from phonenumber_field.formfields import PhoneNumberField # type: ignore
-
+from registrar.models.portfolio import Portfolio
+from registrar.utility.waffle import flag_is_active_anywhere
from django import forms
from django.core.validators import RegexValidator, MaxLengthValidator
from django.utils.safestring import mark_safe
@@ -321,7 +322,8 @@ class OrganizationContactForm(RegistrarForm):
# if it has been filled in when required.
# uncomment to see if modelChoiceField can be an arg later
required=False,
- queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
+ # We populate this queryset in init.
+ queryset=FederalAgency.objects.none(),
widget=ComboboxWidget,
)
organization_name = forms.CharField(
@@ -363,6 +365,20 @@ class OrganizationContactForm(RegistrarForm):
label="Urbanization (required for Puerto Rico only)",
)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Set the queryset for federal agency.
+ # If the organization_requests flag is active, We want to exclude agencies with a portfolio.
+ federal_agency_queryset = FederalAgency.objects.exclude(agency__in=self.excluded_agencies)
+ if flag_is_active_anywhere("organization_feature") and flag_is_active_anywhere("organization_requests"):
+ # Exclude both predefined agencies and those matching portfolio records in one query
+ federal_agency_queryset = federal_agency_queryset.exclude(
+ id__in=Portfolio.objects.values_list("federal_agency__id", flat=True)
+ )
+
+ self.fields["federal_agency"].queryset = federal_agency_queryset
+
def clean_federal_agency(self):
"""Require something to be selected when this is a federal agency."""
federal_agency = self.cleaned_data.get("federal_agency", None)
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index c5a0926ad..3071d40d8 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -15,8 +15,7 @@
from auditlog.models import LogEntry
from django.core.exceptions import ValidationError
-from registrar.utility.waffle import flag_is_active_for_user
-
+from registrar.utility.waffle import flag_is_active_for_user, flag_is_active_anywhere
from .utility.time_stamped_model import TimeStampedModel
from ..utility.email import send_templated_email, EmailSendingError
from itertools import chain
@@ -1299,6 +1298,40 @@ def unlock_requesting_entity(self) -> bool:
return True
return False
+ def unlock_organization_contact(self) -> bool:
+ """Unlocks the organization_contact step."""
+ if flag_is_active_anywhere("organization_feature") and flag_is_active_anywhere("organization_requests"):
+ # Check if the current federal agency is an outlawed one
+ if self.organization_type == self.OrganizationChoices.FEDERAL and self.federal_agency:
+ Portfolio = apps.get_model("registrar.Portfolio")
+ return (
+ FederalAgency.objects.exclude(
+ id__in=Portfolio.objects.values_list("federal_agency__id", flat=True),
+ )
+ .filter(id=self.federal_agency.id)
+ .exists()
+ )
+ return bool(
+ self.federal_agency is not None
+ or self.organization_name is not None
+ or self.address_line1 is not None
+ or self.city is not None
+ or self.state_territory is not None
+ or self.zipcode is not None
+ or self.urbanization is not None
+ )
+
+ def unlock_other_contacts(self) -> bool:
+ """Unlocks the other contacts step"""
+ other_contacts_filled_out = self.other_contacts.filter(
+ first_name__isnull=False,
+ last_name__isnull=False,
+ title__isnull=False,
+ email__isnull=False,
+ phone__isnull=False,
+ ).exists()
+ return (self.has_other_contacts() and other_contacts_filled_out) or self.no_other_contacts_rationale is not None
+
# ## Form policies ## #
#
# These methods control what questions need to be answered by applicants
@@ -1396,140 +1429,6 @@ def get_formatted_cisa_rep_name(self):
names = [n for n in [self.cisa_representative_first_name, self.cisa_representative_last_name] if n]
return " ".join(names) if names else "Unknown"
- def _is_federal_complete(self):
- # Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None
- return not (self.federal_type is None or self.federal_agency is None)
-
- def _is_interstate_complete(self):
- # Interstate -> "About your organization" page can't be empty
- return self.about_your_organization is not None
-
- def _is_state_or_territory_complete(self):
- # State -> ""Election office" page can't be empty
- return self.is_election_board is not None
-
- def _is_tribal_complete(self):
- # Tribal -> "Tribal name" and "Election office" page can't be empty
- return self.tribe_name is not None and self.is_election_board is not None
-
- def _is_county_complete(self):
- # County -> "Election office" page can't be empty
- return self.is_election_board is not None
-
- def _is_city_complete(self):
- # City -> "Election office" page can't be empty
- return self.is_election_board is not None
-
- def _is_special_district_complete(self):
- # Special District -> "Election office" and "About your organization" page can't be empty
- return self.is_election_board is not None and self.about_your_organization is not None
-
- # Do we still want to test this after creator is autogenerated? Currently it went back to being selectable
- def _is_creator_complete(self):
- return self.creator is not None
-
- def _is_organization_name_and_address_complete(self):
- return not (
- self.organization_name is None
- and self.address_line1 is None
- and self.city is None
- and self.state_territory is None
- and self.zipcode is None
- )
-
- def _is_senior_official_complete(self):
- return self.senior_official is not None
-
- def _is_requested_domain_complete(self):
- return self.requested_domain is not None
-
- def _is_purpose_complete(self):
- return self.purpose is not None
-
- def _has_other_contacts_and_filled(self):
- # Other Contacts Radio button is Yes and if all required fields are filled
- return (
- self.has_other_contacts()
- and self.other_contacts.filter(
- first_name__isnull=False,
- last_name__isnull=False,
- title__isnull=False,
- email__isnull=False,
- phone__isnull=False,
- ).exists()
- )
-
- def _has_no_other_contacts_gives_rationale(self):
- # Other Contacts Radio button is No and a rationale is provided
- return self.has_other_contacts() is False and self.no_other_contacts_rationale is not None
-
- def _is_other_contacts_complete(self):
- if self._has_other_contacts_and_filled() or self._has_no_other_contacts_gives_rationale():
- return True
- return False
-
- def _cisa_rep_check(self):
- # Either does not have a CISA rep, OR has a CISA rep + both first name
- # and last name are NOT empty and are NOT an empty string
- to_return = (
- self.has_cisa_representative is True
- and self.cisa_representative_first_name is not None
- and self.cisa_representative_first_name != ""
- and self.cisa_representative_last_name is not None
- and self.cisa_representative_last_name != ""
- ) or self.has_cisa_representative is False
-
- return to_return
-
- def _anything_else_radio_button_and_text_field_check(self):
- # Anything else boolean is True + filled text field and it's not an empty string OR the boolean is No
- return (
- self.has_anything_else_text is True and self.anything_else is not None and self.anything_else != ""
- ) or self.has_anything_else_text is False
-
- def _is_additional_details_complete(self):
- return self._cisa_rep_check() and self._anything_else_radio_button_and_text_field_check()
-
- def _is_policy_acknowledgement_complete(self):
- return self.is_policy_acknowledged is not None
-
- def _is_general_form_complete(self, request):
- return (
- self._is_creator_complete()
- and self._is_organization_name_and_address_complete()
- and self._is_senior_official_complete()
- and self._is_requested_domain_complete()
- and self._is_purpose_complete()
- and self._is_other_contacts_complete()
- and self._is_additional_details_complete()
- and self._is_policy_acknowledgement_complete()
- )
-
- def _form_complete(self, request):
- match self.generic_org_type:
- case DomainRequest.OrganizationChoices.FEDERAL:
- is_complete = self._is_federal_complete()
- case DomainRequest.OrganizationChoices.INTERSTATE:
- is_complete = self._is_interstate_complete()
- case DomainRequest.OrganizationChoices.STATE_OR_TERRITORY:
- is_complete = self._is_state_or_territory_complete()
- case DomainRequest.OrganizationChoices.TRIBAL:
- is_complete = self._is_tribal_complete()
- case DomainRequest.OrganizationChoices.COUNTY:
- is_complete = self._is_county_complete()
- case DomainRequest.OrganizationChoices.CITY:
- is_complete = self._is_city_complete()
- case DomainRequest.OrganizationChoices.SPECIAL_DISTRICT:
- is_complete = self._is_special_district_complete()
- case DomainRequest.OrganizationChoices.SCHOOL_DISTRICT:
- is_complete = True
- case _:
- # NOTE: Shouldn't happen, this is only if somehow they didn't choose an org type
- is_complete = False
- if not is_complete or not self._is_general_form_complete(request):
- return False
- return True
-
"""The following converted_ property methods get field data from this domain request's portfolio,
if there is an associated portfolio. If not, they return data from the domain request model."""
diff --git a/src/registrar/templates/includes/request_review_steps.html b/src/registrar/templates/includes/request_review_steps.html
index 6151d01a8..f1b13f890 100644
--- a/src/registrar/templates/includes/request_review_steps.html
+++ b/src/registrar/templates/includes/request_review_steps.html
@@ -41,7 +41,7 @@
{% endif %}
{% if step == Step.ORGANIZATION_CONTACT %}
- {% if domain_request.organization_name %}
+ {% if domain_request.unlock_organization_contact %}
{% with title=form_titles|get_item:step value=domain_request %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url address='true' %}
{% endwith %}
@@ -116,7 +116,7 @@
Alternative domains
{% endif %}
{% if step == Step.OTHER_CONTACTS %}
- {% if domain_request.other_contacts.all %}
+ {% if domain_request.unlock_other_contacts %}
{% with title=form_titles|get_item:step value=domain_request.other_contacts.all %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url contact='true' list='true' %}
{% endwith %}
diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py
index 82e3b40bb..35a7d76ac 100644
--- a/src/registrar/tests/test_forms.py
+++ b/src/registrar/tests/test_forms.py
@@ -24,6 +24,7 @@
PortfolioMemberForm,
PortfolioNewMemberForm,
)
+from waffle.models import get_waffle_flag_model
from registrar.models.portfolio import Portfolio
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user import User
@@ -39,6 +40,10 @@ def setUp(self):
self.API_BASE_PATH = "/api/v1/available/?domain="
self.user = get_user_model().objects.create(username="username")
self.factory = RequestFactory()
+ # We use both of these flags in the test. In the normal app these are generated normally.
+ # The alternative syntax is adding the decorator to each test.
+ get_waffle_flag_model().objects.get_or_create(name="organization_feature")
+ get_waffle_flag_model().objects.get_or_create(name="organization_requests")
@less_console_noise_decorator
def test_org_contact_zip_invalid(self):
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 157420be7..82fca3fc4 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -1,9 +1,10 @@
from django.forms import ValidationError
from django.test import TestCase
from unittest.mock import patch
-
+from unittest.mock import Mock
from django.test import RequestFactory
-
+from waffle.models import get_waffle_flag_model
+from registrar.views.domain_request import DomainRequestWizard
from registrar.models import (
Contact,
DomainRequest,
@@ -2105,11 +2106,20 @@ def setUp(self):
anything_else="Anything else",
is_policy_acknowledged=True,
creator=self.user,
+ city="fake",
)
-
self.domain_request.other_contacts.add(other)
self.domain_request.current_websites.add(current)
self.domain_request.alternative_domains.add(alt)
+ self.wizard = DomainRequestWizard()
+ self.wizard._domain_request = self.domain_request
+ self.wizard.request = Mock(user=self.user, session={})
+ self.wizard.kwargs = {"id": self.domain_request.id}
+
+ # We use both of these flags in the test. In the normal app these are generated normally.
+ # The alternative syntax is adding the decorator to each test.
+ get_waffle_flag_model().objects.get_or_create(name="organization_feature")
+ get_waffle_flag_model().objects.get_or_create(name="organization_requests")
def tearDown(self):
super().tearDown()
@@ -2124,30 +2134,31 @@ def tearDownClass(cls):
@less_console_noise_decorator
def test_is_federal_complete(self):
- self.assertTrue(self.domain_request._is_federal_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.federal_type = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_federal_complete())
+ self.domain_request.refresh_from_db()
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_interstate_complete(self):
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
self.domain_request.about_your_organization = "Something something about your organization"
self.domain_request.save()
- self.assertTrue(self.domain_request._is_interstate_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.about_your_organization = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_interstate_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_state_or_territory_complete(self):
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
self.domain_request.is_election_board = True
self.domain_request.save()
- self.assertTrue(self.domain_request._is_state_or_territory_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_state_or_territory_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_tribal_complete(self):
@@ -2155,33 +2166,33 @@ def test_is_tribal_complete(self):
self.domain_request.tribe_name = "Tribe Name"
self.domain_request.is_election_board = False
self.domain_request.save()
- self.assertTrue(self.domain_request._is_tribal_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_tribal_complete())
+ self.assertFalse(self.wizard.form_is_complete())
self.domain_request.tribe_name = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_tribal_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_county_complete(self):
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY
self.domain_request.is_election_board = False
self.domain_request.save()
- self.assertTrue(self.domain_request._is_county_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_county_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_city_complete(self):
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY
self.domain_request.is_election_board = False
self.domain_request.save()
- self.assertTrue(self.domain_request._is_city_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_city_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_special_district_complete(self):
@@ -2189,55 +2200,55 @@ def test_is_special_district_complete(self):
self.domain_request.about_your_organization = "Something something about your organization"
self.domain_request.is_election_board = False
self.domain_request.save()
- self.assertTrue(self.domain_request._is_special_district_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_special_district_complete())
+ self.assertFalse(self.wizard.form_is_complete())
self.domain_request.about_your_organization = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_special_district_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_organization_name_and_address_complete(self):
- self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.organization_name = None
self.domain_request.address_line1 = None
self.domain_request.save()
- self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
+ self.assertTrue(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_senior_official_complete(self):
- self.assertTrue(self.domain_request._is_senior_official_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.senior_official = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_senior_official_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_requested_domain_complete(self):
- self.assertTrue(self.domain_request._is_requested_domain_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.requested_domain = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_requested_domain_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_purpose_complete(self):
- self.assertTrue(self.domain_request._is_purpose_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.purpose = None
self.domain_request.save()
- self.assertFalse(self.domain_request._is_purpose_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_other_contacts_complete_missing_one_field(self):
- self.assertTrue(self.domain_request._is_other_contacts_complete())
+ self.assertTrue(self.wizard.form_is_complete())
contact = self.domain_request.other_contacts.first()
contact.first_name = None
contact.save()
- self.assertFalse(self.domain_request._is_other_contacts_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_other_contacts_complete_all_none(self):
self.domain_request.other_contacts.clear()
- self.assertFalse(self.domain_request._is_other_contacts_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_other_contacts_False_and_has_rationale(self):
@@ -2245,7 +2256,7 @@ def test_is_other_contacts_False_and_has_rationale(self):
self.domain_request.other_contacts.clear()
self.domain_request.other_contacts.exists = False
self.domain_request.no_other_contacts_rationale = "Some rationale"
- self.assertTrue(self.domain_request._is_other_contacts_complete())
+ self.assertTrue(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_other_contacts_False_and_NO_rationale(self):
@@ -2253,7 +2264,7 @@ def test_is_other_contacts_False_and_NO_rationale(self):
self.domain_request.other_contacts.clear()
self.domain_request.other_contacts.exists = False
self.domain_request.no_other_contacts_rationale = None
- self.assertFalse(self.domain_request._is_other_contacts_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_is_additional_details_complete(self):
@@ -2457,28 +2468,28 @@ def test_is_additional_details_complete(self):
self.domain_request.save()
self.domain_request.refresh_from_db()
self.assertEqual(
- self.domain_request._is_additional_details_complete(),
+ self.wizard.form_is_complete(),
case["expected"],
msg=f"Failed for case: {case}",
)
@less_console_noise_decorator
def test_is_policy_acknowledgement_complete(self):
- self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.is_policy_acknowledged = False
- self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.is_policy_acknowledged = None
- self.assertFalse(self.domain_request._is_policy_acknowledgement_complete())
+ self.assertFalse(self.wizard.form_is_complete())
@less_console_noise_decorator
def test_form_complete(self):
request = self.factory.get("/")
request.user = self.user
- self.assertTrue(self.domain_request._form_complete(request))
+ self.assertTrue(self.wizard.form_is_complete())
self.domain_request.generic_org_type = None
self.domain_request.save()
- self.assertFalse(self.domain_request._form_complete(request))
+ self.assertFalse(self.wizard.form_is_complete())
class TestPortfolio(TestCase):
diff --git a/src/registrar/tests/test_utilities.py b/src/registrar/tests/test_utilities.py
index 5a2234d66..d882fdedd 100644
--- a/src/registrar/tests/test_utilities.py
+++ b/src/registrar/tests/test_utilities.py
@@ -1,7 +1,8 @@
from django.test import TestCase
from registrar.models import User
from waffle.testutils import override_flag
-from registrar.utility.waffle import flag_is_active_for_user
+from waffle.models import get_waffle_flag_model
+from registrar.utility.waffle import flag_is_active_for_user, flag_is_active_anywhere
class FlagIsActiveForUserTest(TestCase):
@@ -21,3 +22,40 @@ def test_flag_inactive_for_user(self):
# Test that the flag is inactive for the user
is_active = flag_is_active_for_user(self.user, "test_flag")
self.assertFalse(is_active)
+
+
+class TestFlagIsActiveAnywhere(TestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(username="testuser")
+ self.flag_name = "test_flag"
+
+ @override_flag("test_flag", active=True)
+ def test_flag_active_for_everyone(self):
+ """Test when flag is active for everyone"""
+ is_active = flag_is_active_anywhere("test_flag")
+ self.assertTrue(is_active)
+
+ @override_flag("test_flag", active=False)
+ def test_flag_inactive_for_everyone(self):
+ """Test when flag is inactive for everyone"""
+ is_active = flag_is_active_anywhere("test_flag")
+ self.assertFalse(is_active)
+
+ def test_flag_active_for_some_users(self):
+ """Test when flag is active for specific users"""
+ flag, _ = get_waffle_flag_model().objects.get_or_create(name="test_flag")
+ flag.everyone = None
+ flag.save()
+ flag.users.add(self.user)
+
+ is_active = flag_is_active_anywhere("test_flag")
+ self.assertTrue(is_active)
+
+ def test_flag_inactive_with_no_users(self):
+ """Test when flag has no users and everyone is None"""
+ flag, _ = get_waffle_flag_model().objects.get_or_create(name="test_flag")
+ flag.everyone = None
+ flag.save()
+
+ is_active = flag_is_active_anywhere("test_flag")
+ self.assertFalse(is_active)
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 81beba604..f6eba2a56 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -3079,19 +3079,16 @@ def test_unlocked_steps_partial_domain_request(self):
# Create the site and contacts to delete (orphaned)
contact = Contact.objects.create(
- first_name="Henry",
- last_name="Mcfakerson",
+ first_name="Henry", last_name="Mcfakerson", title="test", email="moar@igorville.gov", phone="1234567890"
)
# Create two non-orphaned contacts
contact_2 = Contact.objects.create(
- first_name="Saturn",
- last_name="Mars",
+ first_name="Saturn", last_name="Mars", title="test", email="moar@igorville.gov", phone="1234567890"
)
# Attach a user object to a contact (should not be deleted)
contact_user, _ = Contact.objects.get_or_create(
- first_name="Hank",
- last_name="McFakey",
+ first_name="Hank", last_name="McFakey", title="test", email="moar@igorville.gov", phone="1234567890"
)
site = DraftDomain.objects.create(name="igorville.gov")
@@ -3221,6 +3218,37 @@ def test_wizard_steps_portfolio(self):
federal_agency.delete()
domain_request.delete()
+ @override_flag("organization_feature", active=True)
+ @override_flag("organization_requests", active=True)
+ @less_console_noise_decorator
+ def test_unlock_organization_contact_flags_enabled(self):
+ """Tests unlock_organization_contact when agency exists in a portfolio"""
+ # Create a federal agency
+ federal_agency = FederalAgency.objects.create(agency="Portfolio Agency")
+
+ # Create a portfolio with matching organization name
+ Portfolio.objects.create(
+ creator=self.user, organization_name=federal_agency.agency, federal_agency=federal_agency
+ )
+
+ # Create domain request with the portfolio agency
+ domain_request = completed_domain_request(federal_agency=federal_agency, user=self.user)
+ self.assertFalse(domain_request.unlock_organization_contact())
+
+ @override_flag("organization_feature", active=False)
+ @override_flag("organization_requests", active=False)
+ @less_console_noise_decorator
+ def test_unlock_organization_contact_flags_disabled(self):
+ """Tests unlock_organization_contact when organization flags are disabled"""
+ # Create a federal agency
+ federal_agency = FederalAgency.objects.create(agency="Portfolio Agency")
+
+ # Create a portfolio with matching organization name
+ Portfolio.objects.create(creator=self.user, organization_name=federal_agency.agency)
+
+ domain_request = completed_domain_request(federal_agency=federal_agency, user=self.user)
+ self.assertTrue(domain_request.unlock_organization_contact())
+
class TestPortfolioDomainRequestViewonly(TestWithUser, WebTest):
diff --git a/src/registrar/utility/waffle.py b/src/registrar/utility/waffle.py
index a78799e4c..3071fbed9 100644
--- a/src/registrar/utility/waffle.py
+++ b/src/registrar/utility/waffle.py
@@ -1,5 +1,6 @@
from django.http import HttpRequest
from waffle.decorators import flag_is_active
+from waffle.models import get_waffle_flag_model
def flag_is_active_for_user(user, flag_name):
@@ -10,3 +11,21 @@ def flag_is_active_for_user(user, flag_name):
request = HttpRequest()
request.user = user
return flag_is_active(request, flag_name)
+
+
+def flag_is_active_anywhere(flag_name):
+ """Checks if the given flag name is active for anyone, anywhere.
+ More specifically, it checks on flag.everyone or flag.users.exists().
+ Does not check self.superuser, self.staff or self.group.
+
+ This function effectively behaves like a switch:
+ If said flag is enabled for someone, somewhere - return true.
+ Otherwise - return false.
+ """
+ try:
+ flag = get_waffle_flag_model().get(flag_name)
+ if flag.everyone is None:
+ return flag.users.exists()
+ return flag.everyone
+ except get_waffle_flag_model().DoesNotExist:
+ return False
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index 9754b0d0c..3248c1368 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -107,15 +107,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
Step.TRIBAL_GOVERNMENT: lambda self: self.domain_request.tribe_name is not None,
Step.ORGANIZATION_FEDERAL: lambda self: self.domain_request.federal_type is not None,
Step.ORGANIZATION_ELECTION: lambda self: self.domain_request.is_election_board is not None,
- Step.ORGANIZATION_CONTACT: lambda self: (
- self.domain_request.federal_agency is not None
- or self.domain_request.organization_name is not None
- or self.domain_request.address_line1 is not None
- or self.domain_request.city is not None
- or self.domain_request.state_territory is not None
- or self.domain_request.zipcode is not None
- or self.domain_request.urbanization is not None
- ),
+ Step.ORGANIZATION_CONTACT: lambda self: self.from_model("unlock_organization_contact", False),
Step.ABOUT_YOUR_ORGANIZATION: lambda self: self.domain_request.about_your_organization is not None,
Step.SENIOR_OFFICIAL: lambda self: self.domain_request.senior_official is not None,
Step.CURRENT_SITES: lambda self: (
@@ -123,9 +115,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
),
Step.DOTGOV_DOMAIN: lambda self: self.domain_request.requested_domain is not None,
Step.PURPOSE: lambda self: self.domain_request.purpose is not None,
- Step.OTHER_CONTACTS: lambda self: (
- self.domain_request.other_contacts.exists() or self.domain_request.no_other_contacts_rationale is not None
- ),
+ Step.OTHER_CONTACTS: lambda self: self.from_model("unlock_other_contacts", False),
Step.ADDITIONAL_DETAILS: lambda self: (
# Additional details is complete as long as "has anything else" and "has cisa rep" are not None
(
@@ -434,20 +424,28 @@ def db_check_for_unlocking_steps(self):
Queries the DB for a domain request and returns a list of unlocked steps."""
return [key for key, is_unlocked_checker in self.unlocking_steps.items() if is_unlocked_checker(self)]
+ def form_is_complete(self):
+ """Determines if all required steps in the domain request form are complete.
+ Returns:
+ bool: True if all required steps are complete, False otherwise
+ """
+ # 1. Get all steps visibly present to the user (required steps)
+ # 2. Return every possible step that is "unlocked" (even hidden, conditional ones)
+ # 3. Narrows down the list to remove hidden conditional steps
+ required_steps = set(self.steps.all)
+ unlockable_steps = {step.value for step in self.db_check_for_unlocking_steps()}
+ unlocked_steps = {step for step in required_steps if step in unlockable_steps}
+ return required_steps == unlocked_steps
+
def get_context_data(self):
"""Define context for access on all wizard pages."""
-
requested_domain_name = None
if self.domain_request.requested_domain is not None:
requested_domain_name = self.domain_request.requested_domain.name
context = {}
-
- # Note: we will want to consolidate the non_org_steps_complete check into the same check that
- # org_steps_complete is using at some point.
- non_org_steps_complete = DomainRequest._form_complete(self.domain_request, self.request)
- org_steps_complete = len(self.db_check_for_unlocking_steps()) == len(self.steps)
- if (not self.is_portfolio and non_org_steps_complete) or (self.is_portfolio and org_steps_complete):
+ org_steps_complete = self.form_is_complete()
+ if org_steps_complete:
context = {
"form_titles": self.titles,
"steps": self.steps,
@@ -782,7 +780,8 @@ class Review(DomainRequestWizard):
forms = [] # type: ignore
def get_context_data(self):
- if DomainRequest._form_complete(self.domain_request, self.request) is False:
+ form_complete = self.form_is_complete()
+ if form_complete is False:
logger.warning("User arrived at review page with an incomplete form.")
context = super().get_context_data()
context["Step"] = self.get_step_enum().__members__