updated to main

This commit is contained in:
asaki222 2025-02-05 10:08:48 -05:00
commit 9e038d1f93
No known key found for this signature in database
GPG key ID: C51913A3A09FDC03
14 changed files with 240 additions and 241 deletions

View file

@ -20,7 +20,6 @@ from registrar.views.report_views import (
AnalyticsView,
ExportDomainRequestDataFull,
ExportDataTypeUser,
ExportDataTypeRequests,
ExportMembersPortfolio,
)
@ -260,11 +259,6 @@ urlpatterns = [
ExportDataTypeUser.as_view(),
name="export_data_type_user",
),
path(
"reports/export_data_type_requests/",
ExportDataTypeRequests.as_view(),
name="export_data_type_requests",
),
path(
"domain-request/<int:id>/edit/",
views.DomainRequestWizard.as_view(),

View file

@ -2,7 +2,8 @@ from __future__ import annotations # allows forward references in annotations
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)

View file

@ -15,8 +15,7 @@ from registrar.utility.constants import BranchChoices
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 @@ class DomainRequest(TimeStampedModel):
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 @@ class DomainRequest(TimeStampedModel):
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."""

View file

@ -51,20 +51,7 @@
</form>
</section>
</div>
{% if portfolio %}
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}" id="export-csv">
<section aria-label="Domain Requests report component" class="margin-top-205">
<!----------------------------------------------------------------------
This link is commented out because we intend to add it back in later.
------------------------------------------------------------------------->
<!-- <a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right">
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
</svg>Export as CSV
</a> -->
</section>
</div>
{% endif %}
</div>
{% if portfolio %}

View file

@ -92,11 +92,13 @@
{% endif %}
{% if has_organization_members_flag %}
{% if has_view_members_portfolio_permission %}
<li class="usa-nav__primary-item">
<a href="{% url 'members' %}" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
Members
</a>
</li>
{% endif %}
{% endif %}
<li class="usa-nav__primary-item">

View file

@ -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 @@
{% 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 %}

View file

@ -24,6 +24,7 @@ from registrar.forms.portfolio import (
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 @@ class TestFormValidation(MockEppLib):
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):

View file

@ -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 @@ class TestDomainRequestIncomplete(TestCase):
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 @@ class TestDomainRequestIncomplete(TestCase):
@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 @@ class TestDomainRequestIncomplete(TestCase):
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 @@ class TestDomainRequestIncomplete(TestCase):
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 @@ class TestDomainRequestIncomplete(TestCase):
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 @@ class TestDomainRequestIncomplete(TestCase):
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 @@ class TestDomainRequestIncomplete(TestCase):
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):

View file

@ -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 @@ class FlagIsActiveForUserTest(TestCase):
# 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)

View file

@ -1097,8 +1097,10 @@ class TestPortfolio(WebTest):
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_no_permissions(self):
"""Test the nav contains a link to the no requests page"""
"""Test the nav contains a link to the no requests page
Also test that members link not present"""
UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
@ -1118,20 +1120,23 @@ class TestPortfolio(WebTest):
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
# link to requests
self.assertNotContains(portfolio_landing_page, 'href="/requests/')
# link to create
# link to create request
self.assertNotContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertNotContains(portfolio_landing_page, 'href="/members/')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_all_permissions(self):
"""Test the nav contains a dropdown with a link to create and another link to view requests
Also test for the existence of the Create a new request btn on the requests page"""
Also test for the existence of the Create a new request btn on the requests page
Also test for the existence of the members link"""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
)
self.client.force_login(self.user)
# create and submit a domain request
@ -1151,6 +1156,8 @@ class TestPortfolio(WebTest):
self.assertContains(portfolio_landing_page, 'href="/requests/')
# link to create
self.assertContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertContains(portfolio_landing_page, 'href="/members/')
requests_page = self.client.get(reverse("domain-requests"))
@ -1160,15 +1167,18 @@ class TestPortfolio(WebTest):
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_view_but_not_edit_permissions(self):
"""Test the nav contains a simple link to view requests
Also test for the existence of the Create a new request btn on the requests page"""
Also test for the existence of the Create a new request btn on the requests page
Also test for the existence of members link"""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
self.client.force_login(self.user)
@ -1189,6 +1199,8 @@ class TestPortfolio(WebTest):
self.assertContains(portfolio_landing_page, 'href="/requests/')
# link to create
self.assertNotContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertContains(portfolio_landing_page, 'href="/members/')
requests_page = self.client.get(reverse("domain-requests"))

View file

@ -3079,19 +3079,16 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
# 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 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
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):

View file

@ -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

View file

@ -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 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
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__

View file

@ -201,17 +201,6 @@ class ExportMembersPortfolio(PortfolioReportsPermission, View):
return response
class ExportDataTypeRequests(DomainAndRequestsReportsPermission, View):
"""Returns a domain requests report for a given user on the request"""
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="domain-requests.csv"'
csv_export.DomainRequestDataType.export_data_to_csv(response, request=request)
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDataFull(View):
def get(self, request, *args, **kwargs):