mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-30 06:26:34 +02:00
Merge branch 'main' into za/2927-blocked-from-starting-requests
This commit is contained in:
commit
d21695316c
20 changed files with 1117 additions and 118 deletions
|
@ -20,6 +20,7 @@
|
|||
"http://localhost:8080/request/anything_else/",
|
||||
"http://localhost:8080/request/requirements/",
|
||||
"http://localhost:8080/request/finished/",
|
||||
"http://localhost:8080/request/requesting_entity/",
|
||||
"http://localhost:8080/user-profile/",
|
||||
"http://localhost:8080/members/",
|
||||
"http://localhost:8080/members/new-member"
|
||||
|
|
|
@ -5,6 +5,7 @@ from django import forms
|
|||
from django.db.models import Value, CharField, Q
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
from django.http import HttpResponseRedirect
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.utility.admin_helpers import (
|
||||
get_action_needed_reason_default_email,
|
||||
get_rejection_reason_default_email,
|
||||
|
@ -28,6 +29,7 @@ from waffle.models import Sample, Switch
|
|||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||
from django.contrib.admin.views.main import ORDER_VAR
|
||||
from registrar.widgets import NoAutocompleteFilteredSelectMultiple
|
||||
|
@ -1478,7 +1480,18 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
search_help_text = "Search by domain."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["portfolio", "sub_organization", "creator", "domain_request", "notes"]}),
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": [
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"creator",
|
||||
"domain_request",
|
||||
"notes",
|
||||
]
|
||||
},
|
||||
),
|
||||
(".gov domain", {"fields": ["domain"]}),
|
||||
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||
("Background info", {"fields": ["anything_else"]}),
|
||||
|
@ -1748,6 +1761,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"fields": [
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
"status_history",
|
||||
"status",
|
||||
"rejection_reason",
|
||||
|
@ -1849,6 +1865,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
]
|
||||
autocomplete_fields = [
|
||||
"approved_domain",
|
||||
|
@ -1868,6 +1887,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
change_form_template = "django/admin/domain_request_change_form.html"
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
fieldsets = super().get_fieldsets(request, obj)
|
||||
|
||||
# Hide certain suborg fields behind the organization feature flag
|
||||
# if it is not enabled
|
||||
if not flag_is_active_for_user(request.user, "organization_feature"):
|
||||
excluded_fields = [
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
]
|
||||
modified_fieldsets = []
|
||||
for name, data in fieldsets:
|
||||
fields = data.get("fields", [])
|
||||
fields = tuple(field for field in fields if field not in excluded_fields)
|
||||
modified_fieldsets.append((name, {**data, "fields": fields}))
|
||||
return modified_fieldsets
|
||||
return fieldsets
|
||||
|
||||
# Trigger action when a fieldset is changed
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Custom save_model definition that handles edge cases"""
|
||||
|
@ -3206,6 +3244,14 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
# straightforward and the readonly_fields list can control their behavior
|
||||
readonly_fields.extend([field.name for field in self.model._meta.fields])
|
||||
|
||||
# Make senior_official readonly for federal organizations
|
||||
if obj and obj.organization_type == obj.OrganizationChoices.FEDERAL:
|
||||
if "senior_official" not in readonly_fields:
|
||||
readonly_fields.append("senior_official")
|
||||
elif "senior_official" in readonly_fields:
|
||||
# Remove senior_official from readonly_fields if org is non-federal
|
||||
readonly_fields.remove("senior_official")
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
|
||||
|
@ -3228,12 +3274,11 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
extra_context["domain_requests"] = obj.get_domain_requests(order_by=["requested_domain__name"])
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
||||
def save_model(self, request, obj: Portfolio, form, change):
|
||||
if hasattr(obj, "creator") is False:
|
||||
# ---- update creator ----
|
||||
# Set the creator field to the current admin user
|
||||
obj.creator = request.user if request.user.is_authenticated else None
|
||||
obj.creator = request.user if request.user.is_authenticated else None # type: ignore
|
||||
# ---- update organization name ----
|
||||
# org name will be the same as federal agency, if it is federal,
|
||||
# otherwise it will be the actual org name. If nothing is entered for
|
||||
|
@ -3243,12 +3288,19 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
if is_federal and obj.organization_name is None:
|
||||
obj.organization_name = obj.federal_agency.agency
|
||||
|
||||
# Remove this line when senior_official is no longer readonly in /admin.
|
||||
# Set the senior official field to the senior official on the federal agency
|
||||
# when federal - otherwise, clear the field.
|
||||
if obj.organization_type == obj.OrganizationChoices.FEDERAL:
|
||||
if obj.federal_agency:
|
||||
if obj.federal_agency.so_federal_agency.exists():
|
||||
obj.senior_official = obj.federal_agency.so_federal_agency.first()
|
||||
else:
|
||||
obj.senior_official = None
|
||||
else:
|
||||
if obj.federal_agency and obj.federal_agency.agency != "Non-Federal Agency":
|
||||
if obj.federal_agency.so_federal_agency.first() == obj.senior_official:
|
||||
obj.senior_official = None
|
||||
obj.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() # type: ignore
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
|
|
@ -47,10 +47,49 @@ function addOrRemoveSessionBoolean(name, add){
|
|||
|
||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||
// Event handlers.
|
||||
/** Helper function that handles business logic for the suborganization field.
|
||||
* Can be used anywhere the suborganization dropdown exists
|
||||
*/
|
||||
function handleSuborganizationFields(
|
||||
portfolioDropdownSelector="#id_portfolio",
|
||||
suborgDropdownSelector="#id_sub_organization",
|
||||
requestedSuborgFieldSelector=".field-requested_suborganization",
|
||||
suborgCitySelector=".field-suborganization_city",
|
||||
suborgStateTerritorySelector=".field-suborganization_state_territory"
|
||||
) {
|
||||
// These dropdown are select2 fields so they must be interacted with via jquery
|
||||
const portfolioDropdown = django.jQuery(portfolioDropdownSelector)
|
||||
const suborganizationDropdown = django.jQuery(suborgDropdownSelector)
|
||||
const requestedSuborgField = document.querySelector(requestedSuborgFieldSelector);
|
||||
const suborgCity = document.querySelector(suborgCitySelector);
|
||||
const suborgStateTerritory = document.querySelector(suborgStateTerritorySelector);
|
||||
if (!suborganizationDropdown || !requestedSuborgField || !suborgCity || !suborgStateTerritory) {
|
||||
console.error("Requested suborg fields not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
function toggleSuborganizationFields() {
|
||||
if (portfolioDropdown.val() && !suborganizationDropdown.val()) {
|
||||
showElement(requestedSuborgField);
|
||||
showElement(suborgCity);
|
||||
showElement(suborgStateTerritory);
|
||||
}else {
|
||||
hideElement(requestedSuborgField);
|
||||
hideElement(suborgCity);
|
||||
hideElement(suborgStateTerritory);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the function once on page startup, then attach an event listener
|
||||
toggleSuborganizationFields();
|
||||
suborganizationDropdown.on("change", toggleSuborganizationFields);
|
||||
portfolioDropdown.on("change", toggleSuborganizationFields);
|
||||
}
|
||||
|
||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||
// Initialization code.
|
||||
|
||||
|
||||
/** An IIFE for pages in DjangoAdmin that use modals.
|
||||
* Dja strips out form elements, and modals generate their content outside
|
||||
* of the current form scope, so we need to "inject" these inputs.
|
||||
|
@ -927,6 +966,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
// This is the additional information that exists beneath the SO element.
|
||||
var contactList = document.querySelector(".field-senior_official .dja-address-contact-list");
|
||||
const federalAgencyContainer = document.querySelector(".field-federal_agency");
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
let isPortfolioPage = document.getElementById("portfolio_form");
|
||||
|
@ -975,11 +1015,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
let selectedValue = organizationType.value;
|
||||
if (selectedValue === "federal") {
|
||||
hideElement(organizationNameContainer);
|
||||
showElement(federalAgencyContainer);
|
||||
if (federalType) {
|
||||
showElement(federalType);
|
||||
}
|
||||
} else {
|
||||
showElement(organizationNameContainer);
|
||||
hideElement(federalAgencyContainer);
|
||||
if (federalType) {
|
||||
hideElement(federalType);
|
||||
}
|
||||
|
@ -1170,3 +1212,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
};
|
||||
}
|
||||
})();
|
||||
|
||||
/** An IIFE for dynamic DomainRequest fields
|
||||
*/
|
||||
(function dynamicDomainRequestFields(){
|
||||
const domainRequestPage = document.getElementById("domainrequest_form");
|
||||
if (domainRequestPage) {
|
||||
handleSuborganizationFields();
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
/** An IIFE for dynamic DomainInformation fields
|
||||
*/
|
||||
(function dynamicDomainInformationFields(){
|
||||
const domainInformationPage = document.getElementById("domaininformation_form");
|
||||
// DomainInformation is embedded inside domain so this should fire there too
|
||||
const domainPage = document.getElementById("domain_form");
|
||||
if (domainInformationPage) {
|
||||
handleSuborganizationFields();
|
||||
}
|
||||
|
||||
if (domainPage) {
|
||||
handleSuborganizationFields(portfolioDropdownSelector="#id_domain_info-0-portfolio", suborgDropdownSelector="#id_domain_info-0-sub_organization");
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -2775,3 +2775,48 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
}
|
||||
})();
|
||||
|
||||
/** An IIFE that intializes the requesting entity page.
|
||||
* This page has a radio button that dynamically toggles some fields
|
||||
* Within that, the dropdown also toggles some additional form elements.
|
||||
*/
|
||||
(function handleRequestingEntityFieldset() {
|
||||
// Sadly, these ugly ids are the auto generated with this prefix
|
||||
const formPrefix = "portfolio_requesting_entity"
|
||||
const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`);
|
||||
const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`);
|
||||
const select = document.getElementById(`id_${formPrefix}-sub_organization`);
|
||||
const suborgContainer = document.getElementById("suborganization-container");
|
||||
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
|
||||
if (!radios || !select || !suborgContainer || !suborgDetailsContainer) return;
|
||||
|
||||
// requestingSuborganization: This just broadly determines if they're requesting a suborg at all
|
||||
// requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not.
|
||||
var requestingSuborganization = Array.from(radios).find(radio => radio.checked)?.value === "True";
|
||||
var requestingNewSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`);
|
||||
|
||||
function toggleSuborganization(radio=null) {
|
||||
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
||||
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
||||
requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False";
|
||||
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
||||
}
|
||||
|
||||
// Add fake "other" option to sub_organization select
|
||||
if (select && !Array.from(select.options).some(option => option.value === "other")) {
|
||||
select.add(new Option("Other (enter your organization manually)", "other"));
|
||||
}
|
||||
|
||||
if (requestingNewSuborganization.value === "True") {
|
||||
select.value = "other";
|
||||
}
|
||||
|
||||
// Add event listener to is_suborganization radio buttons, and run for initial display
|
||||
toggleSuborganization();
|
||||
radios.forEach(radio => {
|
||||
radio.addEventListener("click", () => toggleSuborganization(radio));
|
||||
});
|
||||
|
||||
// Add event listener to the suborg dropdown to show/hide the suborg details section
|
||||
select.addEventListener("change", () => toggleSuborganization());
|
||||
})();
|
||||
|
|
|
@ -13,7 +13,7 @@ from registrar.forms.utility.wizard_form_helper import (
|
|||
BaseYesNoForm,
|
||||
BaseDeletableRegistrarForm,
|
||||
)
|
||||
from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency
|
||||
from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency, Suborganization
|
||||
from registrar.templatetags.url_helpers import public_site_url
|
||||
from registrar.utility.enums import ValidationReturnType
|
||||
from registrar.utility.constants import BranchChoices
|
||||
|
@ -22,10 +22,146 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class RequestingEntityForm(RegistrarForm):
|
||||
organization_name = forms.CharField(
|
||||
label="Organization name",
|
||||
error_messages={"required": "Enter the name of your organization."},
|
||||
"""The requesting entity form contains a dropdown for suborganizations,
|
||||
and some (hidden by default) input fields that allow the user to request for a suborganization.
|
||||
All of these fields are not required by default, but as we use javascript to conditionally show
|
||||
and hide some of these, they then become required in certain circumstances."""
|
||||
|
||||
# IMPORTANT: This is tied to DomainRequest.is_requesting_new_suborganization().
|
||||
# This is due to the from_database method on DomainRequestWizard.
|
||||
# Add a hidden field to store if the user is requesting a new suborganization.
|
||||
# This hidden boolean is used for our javascript to communicate to us and to it.
|
||||
# If true, the suborganization form will auto select a js value "Other".
|
||||
# If this selection is made on the form (tracked by js), then it will toggle the form value of this.
|
||||
# In other words, this essentially tracks if the suborganization field == "Other".
|
||||
# "Other" is just an imaginary value that is otherwise invalid.
|
||||
# Note the logic in `def clean` and `handleRequestingEntityFieldset` in get-gov.js
|
||||
is_requesting_new_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
sub_organization = forms.ModelChoiceField(
|
||||
label="Suborganization name",
|
||||
required=False,
|
||||
queryset=Suborganization.objects.none(),
|
||||
empty_label="--Select--",
|
||||
)
|
||||
requested_suborganization = forms.CharField(
|
||||
label="Requested suborganization",
|
||||
required=False,
|
||||
)
|
||||
suborganization_city = forms.CharField(
|
||||
label="City",
|
||||
required=False,
|
||||
)
|
||||
suborganization_state_territory = forms.ChoiceField(
|
||||
label="State, territory, or military post",
|
||||
required=False,
|
||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Override of init to add the suborganization queryset"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.domain_request.portfolio:
|
||||
self.fields["sub_organization"].queryset = Suborganization.objects.filter(
|
||||
portfolio=self.domain_request.portfolio
|
||||
)
|
||||
|
||||
def clean_sub_organization(self):
|
||||
"""On suborganization clean, set the suborganization value to None if the user is requesting
|
||||
a custom suborganization (as it doesn't exist yet)"""
|
||||
|
||||
# If it's a new suborganization, return None (equivalent to selecting nothing)
|
||||
if self.cleaned_data.get("is_requesting_new_suborganization"):
|
||||
return None
|
||||
|
||||
# Otherwise just return the suborg as normal
|
||||
return self.cleaned_data.get("sub_organization")
|
||||
|
||||
def full_clean(self):
|
||||
"""Validation logic to remove the custom suborganization value before clean is triggered.
|
||||
Without this override, the form will throw an 'invalid option' error."""
|
||||
# Remove the custom other field before cleaning
|
||||
data = self.data.copy() if self.data else None
|
||||
|
||||
# Remove the 'other' value from suborganization if it exists.
|
||||
# This is a special value that tracks if the user is requesting a new suborg.
|
||||
suborganization = self.data.get("portfolio_requesting_entity-sub_organization")
|
||||
if suborganization and "other" in suborganization:
|
||||
data["portfolio_requesting_entity-sub_organization"] = ""
|
||||
|
||||
# Set the modified data back to the form
|
||||
self.data = data
|
||||
|
||||
# Call the parent's full_clean method
|
||||
super().full_clean()
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean implementation to handle our desired logic flow for suborganization.
|
||||
Given that these fields often rely on eachother, we need to do this in the parent function."""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Do some custom error validation if the requesting entity is a suborg.
|
||||
# Otherwise, just validate as normal.
|
||||
suborganization = self.cleaned_data.get("sub_organization")
|
||||
is_requesting_new_suborganization = self.cleaned_data.get("is_requesting_new_suborganization")
|
||||
|
||||
# Get the value of the yes/no checkbox from RequestingEntityYesNoForm.
|
||||
# Since self.data stores this as a string, we need to convert "True" => True.
|
||||
requesting_entity_is_suborganization = self.data.get(
|
||||
"portfolio_requesting_entity-requesting_entity_is_suborganization"
|
||||
)
|
||||
if requesting_entity_is_suborganization == "True":
|
||||
if is_requesting_new_suborganization:
|
||||
# Validate custom suborganization fields
|
||||
if not cleaned_data.get("requested_suborganization"):
|
||||
self.add_error("requested_suborganization", "Requested suborganization is required.")
|
||||
if not cleaned_data.get("suborganization_city"):
|
||||
self.add_error("suborganization_city", "City is required.")
|
||||
if not cleaned_data.get("suborganization_state_territory"):
|
||||
self.add_error("suborganization_state_territory", "State, territory, or military post is required.")
|
||||
elif not suborganization:
|
||||
self.add_error("sub_organization", "Suborganization is required.")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class RequestingEntityYesNoForm(BaseYesNoForm):
|
||||
"""The yes/no field for the RequestingEntity form."""
|
||||
|
||||
# This first option will change dynamically
|
||||
form_choices = ((False, "Current Organization"), (True, "A suborganization. (choose from list)"))
|
||||
|
||||
# IMPORTANT: This is tied to DomainRequest.is_requesting_new_suborganization().
|
||||
# This is due to the from_database method on DomainRequestWizard.
|
||||
field_name = "requesting_entity_is_suborganization"
|
||||
required_error_message = "Requesting entity is required."
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Extend the initialization of the form from RegistrarForm __init__"""
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.domain_request.portfolio:
|
||||
self.form_choices = (
|
||||
(False, self.domain_request.portfolio),
|
||||
(True, "A suborganization (choose from list)"),
|
||||
)
|
||||
self.fields[self.field_name] = self.get_typed_choice_field()
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form.
|
||||
Returns True (checked) if the requesting entity is a suborganization,
|
||||
and False if it is a portfolio. Returns None if neither condition is met.
|
||||
"""
|
||||
# True means that the requesting entity is a suborganization,
|
||||
# whereas False means that the requesting entity is a portfolio.
|
||||
if self.domain_request.requesting_entity_is_suborganization():
|
||||
return True
|
||||
elif self.domain_request.requesting_entity_is_portfolio():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class OrganizationTypeForm(RegistrarForm):
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# Generated by Django 4.2.10 on 2024-11-01 17:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0135_alter_federalagency_agency_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="requested_suborganization",
|
||||
field=models.CharField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="suborganization_city",
|
||||
field=models.CharField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="suborganization_state_territory",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
max_length=2,
|
||||
null=True,
|
||||
verbose_name="state, territory, or military post",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -13,6 +13,8 @@ from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
|||
from registrar.utility.constants import BranchChoices
|
||||
from auditlog.models import LogEntry
|
||||
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
from itertools import chain
|
||||
|
@ -344,6 +346,24 @@ class DomainRequest(TimeStampedModel):
|
|||
verbose_name="Suborganization",
|
||||
)
|
||||
|
||||
requested_suborganization = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
suborganization_city = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
suborganization_state_territory = models.CharField(
|
||||
max_length=2,
|
||||
choices=StateTerritoryChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="state, territory, or military post",
|
||||
)
|
||||
|
||||
# This is the domain request user who created this domain request.
|
||||
creator = models.ForeignKey(
|
||||
"registrar.User",
|
||||
|
@ -823,10 +843,13 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
try:
|
||||
if not context:
|
||||
has_organization_feature_flag = flag_is_active_for_user(recipient, "organization_feature")
|
||||
is_org_user = has_organization_feature_flag and recipient.has_base_portfolio_permission(self.portfolio)
|
||||
context = {
|
||||
"domain_request": self,
|
||||
# This is the user that we refer to in the email
|
||||
"recipient": recipient,
|
||||
"is_org_user": is_org_user,
|
||||
}
|
||||
|
||||
if custom_email_content:
|
||||
|
@ -1102,6 +1125,60 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
self.creator.restrict_user()
|
||||
|
||||
def requesting_entity_is_portfolio(self) -> bool:
|
||||
"""Determines if this record is requesting that a portfolio be their organization.
|
||||
Used for the RequestingEntity page.
|
||||
Returns True if the portfolio exists and if organization_name matches portfolio.organization_name.
|
||||
"""
|
||||
if self.portfolio and self.organization_name == self.portfolio.organization_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def requesting_entity_is_suborganization(self) -> bool:
|
||||
"""Determines if this record is also requesting that it be tied to a suborganization.
|
||||
Used for the RequestingEntity page.
|
||||
Returns True if portfolio exists and either sub_organization exists,
|
||||
or if is_requesting_new_suborganization() is true.
|
||||
Returns False otherwise.
|
||||
"""
|
||||
if self.portfolio and (self.sub_organization or self.is_requesting_new_suborganization()):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_requesting_new_suborganization(self) -> bool:
|
||||
"""Determines if a user is trying to request
|
||||
a new suborganization using the domain request form, rather than one that already exists.
|
||||
Used for the RequestingEntity page.
|
||||
|
||||
Returns True if a sub_organization does not exist and if requested_suborganization,
|
||||
suborganization_city, and suborganization_state_territory all exist.
|
||||
Returns False otherwise.
|
||||
"""
|
||||
|
||||
# If a suborganization already exists, it can't possibly be a new one.
|
||||
# As well, we need all required fields to exist.
|
||||
required_fields = [
|
||||
self.requested_suborganization,
|
||||
self.suborganization_city,
|
||||
self.suborganization_state_territory,
|
||||
]
|
||||
if not self.sub_organization and all(required_fields):
|
||||
return True
|
||||
return False
|
||||
|
||||
# ## Form unlocking steps ## #
|
||||
#
|
||||
# These methods control the conditions in which we should unlock certain domain wizard steps.
|
||||
|
||||
def unlock_requesting_entity(self) -> bool:
|
||||
"""Unlocks the requesting entity step. Used for the RequestingEntity page.
|
||||
Returns true if requesting_entity_is_suborganization() and requesting_entity_is_portfolio().
|
||||
Returns False otherwise.
|
||||
"""
|
||||
if self.requesting_entity_is_suborganization() or self.requesting_entity_is_portfolio():
|
||||
return True
|
||||
return False
|
||||
|
||||
# ## Form policies ## #
|
||||
#
|
||||
# These methods control what questions need to be answered by applicants
|
||||
|
|
|
@ -2,6 +2,8 @@ from django.db import models
|
|||
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.models.user import User
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
@ -131,6 +133,17 @@ class Portfolio(TimeStampedModel):
|
|||
def get_federal_type(cls, federal_agency):
|
||||
return federal_agency.federal_type if federal_agency else None
|
||||
|
||||
@property
|
||||
def portfolio_admin_users(self):
|
||||
"""Gets all users with the role organization_admin for this particular portfolio.
|
||||
Returns a queryset of User."""
|
||||
admin_ids = self.portfolio_users.filter(
|
||||
roles__overlap=[
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
],
|
||||
).values_list("user__id", flat=True)
|
||||
return User.objects.filter(id__in=admin_ids)
|
||||
|
||||
# == Getters for domains == #
|
||||
def get_domains(self, order_by=None):
|
||||
"""Returns all DomainInformations associated with this portfolio"""
|
||||
|
|
|
@ -2,15 +2,58 @@
|
|||
{% load field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>🛸🛸🛸🛸 Placeholder content 🛸🛸🛸🛸</p>
|
||||
<p>To help with our review, we need to understand whether the domain you're requesting will be used by the Department of Energy or by one of its suborganizations.</p>
|
||||
<p>We define a suborganization as any entity (agency, bureau, office) that falls under the overarching organization.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend>
|
||||
<h2>What is the name of your space vessel?</h2>
|
||||
<h2>Who will use the domain you’re requesting?</h2>
|
||||
</legend>
|
||||
|
||||
{% input_with_errors forms.0.organization_name %}
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
|
||||
{# forms.0 is a small yes/no form that toggles the visibility of "requesting entity" formset #}
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% with attr_required=True %}
|
||||
{% input_with_errors forms.0.requesting_entity_is_suborganization %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% comment %} Add an invisible form element to track whether the custom value "other"
|
||||
was selected or not. This allows for persistence across page reloads without using session variables.
|
||||
{% endcomment %}
|
||||
{% with add_group_class="display-none" %}
|
||||
{% input_with_errors forms.1.is_requesting_new_suborganization %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="suborganization-container" class="margin-top-4">
|
||||
<h2>Add suborganization information</h2>
|
||||
<p>
|
||||
This information will be published in <a class="usa-link usa-link--always-blue" href="{% public_site_url 'about/data' %}">.gov’s public data</a>. If you don’t see your suborganization in the list,
|
||||
select “other” and enter the name or your suborganization.
|
||||
</p>
|
||||
{% with attr_required=True %}
|
||||
{% input_with_errors forms.1.sub_organization %}
|
||||
{% endwith %}
|
||||
|
||||
{% comment %} This will be toggled if a special value, "other", is selected.
|
||||
Otherwise this field is invisible.
|
||||
{% endcomment %}
|
||||
<div id="suborganization-container__details">
|
||||
{% with attr_required=True %}
|
||||
{% input_with_errors forms.1.requested_suborganization %}
|
||||
{% endwith %}
|
||||
{% with attr_required=True %}
|
||||
{% input_with_errors forms.1.suborganization_city %}
|
||||
{% endwith %}
|
||||
{% with attr_required=True %}
|
||||
{% input_with_errors forms.1.suborganization_state_territory %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{% load custom_filters %}SUMMARY OF YOUR DOMAIN REQUEST
|
||||
|
||||
Requesting entity: {# if blockmakes a newline #}
|
||||
{{ domain_request|display_requesting_entity }}
|
||||
{% if domain_request.current_websites.exists %}
|
||||
Current websites: {% for site in domain_request.current_websites.all %}
|
||||
{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
{{ domain_request.requested_domain.name }}
|
||||
{% if domain_request.alternative_domains.all %}
|
||||
Alternative domains:
|
||||
{% for site in domain_request.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
Purpose of your domain:
|
||||
{{ domain_request.purpose }}
|
||||
{% if domain_request.anything_else %}
|
||||
Additional details:
|
||||
{{ domain_request.anything_else }}
|
||||
{% endif %}
|
||||
{% if recipient %}
|
||||
Your contact information:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=recipient %}{% endspaceless %}
|
||||
{% endif %}
|
||||
|
||||
Administrators from your organization:{% for admin in domain_request.portfolio.portfolio_admin_users %}
|
||||
{% spaceless %}{% if admin != recipient %}{% include "emails/includes/contact.txt" with contact=admin %}{% endif %}{% endspaceless %}
|
||||
{% endfor %}
|
|
@ -31,7 +31,7 @@ THANK YOU
|
|||
|
||||
----------------------------------------------------------------
|
||||
|
||||
{% include 'emails/includes/domain_request_summary.txt' %}
|
||||
{% if is_org_user %}{% include 'emails/includes/portfolio_domain_request_summary.txt' %}{% else %}{% include 'emails/includes/domain_request_summary.txt' %}{% endif %}
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if has_organization_members_flag %}
|
||||
{% if has_organization_members_flag and not hide_members %}
|
||||
<li class="usa-nav__primary-item">
|
||||
<a href="{% url 'members' %}" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
|
||||
Members
|
||||
|
|
|
@ -8,15 +8,24 @@
|
|||
{% endif %}
|
||||
|
||||
{% if step == Step.REQUESTING_ENTITY %}
|
||||
{% if domain_request.organization_name %}
|
||||
{% 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 %}
|
||||
{% with title=form_titles|get_item:step %}
|
||||
{% if domain_request.sub_organization %}
|
||||
{% include "includes/summary_item.html" with value=domain_request.sub_organization edit_link=domain_request_url %}
|
||||
{% comment %} We don't have city or state_territory for suborganizations yet, so no data should display {% endcomment %}
|
||||
{% elif domain_request.requesting_entity_is_suborganization %}
|
||||
{% include "includes/summary_item.html" with value=domain_request.requested_suborganization edit_link=domain_request_url %}
|
||||
<p class="margin-y-0">{{domain_request.suborganization_city}}, {{domain_request.suborganization_state_territory}}</p>
|
||||
{% elif domain_request.requesting_entity_is_portfolio %}
|
||||
{% include "includes/summary_item.html" with value=domain_request.portfolio.organization_name edit_link=domain_request_url %}
|
||||
{% if domain_request.portfolio.city and domain_request.portfolio.state_territory %}
|
||||
<p class="margin-y-0">{{domain_request.portfolio.city}}, {{domain_request.portfolio.state_territory}}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% with value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
|
|
|
@ -145,6 +145,9 @@
|
|||
{% endblock request_summary_header%}
|
||||
|
||||
{% block request_summary %}
|
||||
{% if portfolio %}
|
||||
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
|
||||
{% else %}
|
||||
{% with heading_level='h3' %}
|
||||
{% with org_type=DomainRequest.get_generic_org_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
|
@ -231,6 +234,7 @@
|
|||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock request_summary%}
|
||||
</div>
|
||||
</main>
|
|
@ -257,3 +257,28 @@ def portfolio_role_summary(user, portfolio):
|
|||
return user.portfolio_role_summary(portfolio)
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
@register.filter(name="display_requesting_entity")
|
||||
def display_requesting_entity(domain_request):
|
||||
"""Workaround for a newline issue in .txt files (our emails) as if statements
|
||||
count as a newline to the file.
|
||||
Will output something that looks like:
|
||||
MyOrganizationName
|
||||
Boise, ID
|
||||
"""
|
||||
display = ""
|
||||
if domain_request.sub_organization:
|
||||
display = domain_request.sub_organization
|
||||
elif domain_request.requesting_entity_is_suborganization():
|
||||
display = (
|
||||
f"{domain_request.requested_suborganization}\n"
|
||||
f"{domain_request.suborganization_city}, {domain_request.suborganization_state_territory}"
|
||||
)
|
||||
elif domain_request.requesting_entity_is_portfolio():
|
||||
display = (
|
||||
f"{domain_request.portfolio.organization_name}\n"
|
||||
f"{domain_request.portfolio.city}, {domain_request.portfolio.state_territory}"
|
||||
)
|
||||
|
||||
return display
|
||||
|
|
|
@ -2103,6 +2103,66 @@ class TestPortfolioAdmin(TestCase):
|
|||
display_members = self.admin.display_members(self.portfolio)
|
||||
self.assertIn(f'<a href="{url}">2 members</a>', display_members)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_senior_official_readonly_for_federal_org(self):
|
||||
"""Test that senior_official field is readonly for federal organizations"""
|
||||
request = self.factory.get("/")
|
||||
request.user = self.superuser
|
||||
|
||||
# Create a federal portfolio
|
||||
portfolio = Portfolio.objects.create(
|
||||
organization_name="Test Federal Org",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
creator=self.superuser,
|
||||
)
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request, portfolio)
|
||||
self.assertIn("senior_official", readonly_fields)
|
||||
|
||||
# Change to non-federal org
|
||||
portfolio.organization_type = DomainRequest.OrganizationChoices.CITY
|
||||
readonly_fields = self.admin.get_readonly_fields(request, portfolio)
|
||||
self.assertNotIn("senior_official", readonly_fields)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_senior_official_auto_assignment(self):
|
||||
"""Test automatic senior official assignment based on organization type and federal agency"""
|
||||
request = self.factory.get("/")
|
||||
request.user = self.superuser
|
||||
|
||||
# Create a federal agency with a senior official
|
||||
federal_agency = FederalAgency.objects.create(agency="Test Agency")
|
||||
senior_official = SeniorOfficial.objects.create(
|
||||
first_name="Test",
|
||||
last_name="Official",
|
||||
title="Some guy",
|
||||
email="test@example.gov",
|
||||
federal_agency=federal_agency,
|
||||
)
|
||||
|
||||
# Create a federal portfolio
|
||||
portfolio = Portfolio.objects.create(
|
||||
organization_name="Test Federal Org",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
creator=self.superuser,
|
||||
)
|
||||
|
||||
# Test that the federal org gets senior official from agency when federal
|
||||
portfolio.federal_agency = federal_agency
|
||||
self.admin.save_model(request, portfolio, form=None, change=False)
|
||||
self.assertEqual(portfolio.senior_official, senior_official)
|
||||
|
||||
# Test non-federal org clears senior official when not city
|
||||
portfolio.organization_type = DomainRequest.OrganizationChoices.CITY
|
||||
self.admin.save_model(request, portfolio, form=None, change=True)
|
||||
self.assertIsNone(portfolio.senior_official)
|
||||
self.assertEqual(portfolio.federal_agency.agency, "Non-Federal Agency")
|
||||
|
||||
# Cleanup
|
||||
senior_official.delete()
|
||||
federal_agency.delete()
|
||||
portfolio.delete()
|
||||
|
||||
|
||||
class TestTransferUser(WebTest):
|
||||
"""User transfer custom admin page"""
|
||||
|
|
|
@ -1642,6 +1642,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"federal_agency",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
"creator",
|
||||
"investigator",
|
||||
"generic_org_type",
|
||||
|
@ -1686,7 +1689,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
request.user = self.staffuser
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
self.maxDiff = None
|
||||
expected_fields = [
|
||||
"other_contacts",
|
||||
"current_websites",
|
||||
|
@ -1706,6 +1709,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
]
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.urls import reverse
|
|||
from api.tests.common import less_console_noise_decorator
|
||||
from registrar.config import settings
|
||||
from registrar.models import Portfolio, SeniorOfficial
|
||||
from unittest.mock import MagicMock
|
||||
from django_webtest import WebTest # type: ignore
|
||||
from registrar.models import (
|
||||
DomainRequest,
|
||||
|
@ -9,13 +10,15 @@ from registrar.models import (
|
|||
DomainInformation,
|
||||
UserDomainRole,
|
||||
User,
|
||||
Suborganization,
|
||||
AllowedEmail,
|
||||
)
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user_group import UserGroup
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.tests.test_views import TestWithUser
|
||||
from .common import MockSESClient, completed_domain_request, create_test_user
|
||||
from .common import MockSESClient, completed_domain_request, create_test_user, create_user
|
||||
from waffle.testutils import override_flag
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
import boto3_mocking # type: ignore
|
||||
|
@ -1592,3 +1595,284 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest):
|
|||
|
||||
# Make sure the response is not found
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
class TestRequestingEntity(WebTest):
|
||||
"""The requesting entity page is a domain request form that only exists
|
||||
within the context of a portfolio."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
self.user = create_user()
|
||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||
self.portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel Alaska")
|
||||
self.suborganization, _ = Suborganization.objects.get_or_create(
|
||||
name="Rocky road",
|
||||
portfolio=self.portfolio,
|
||||
)
|
||||
self.suborganization_2, _ = Suborganization.objects.get_or_create(
|
||||
name="Vanilla",
|
||||
portfolio=self.portfolio,
|
||||
)
|
||||
self.unrelated_suborganization, _ = Suborganization.objects.get_or_create(
|
||||
name="Cold",
|
||||
portfolio=self.portfolio_2,
|
||||
)
|
||||
self.portfolio_role = UserPortfolioPermission.objects.create(
|
||||
portfolio=self.portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
# Login the current user
|
||||
self.app.set_user(self.user.username)
|
||||
|
||||
self.mock_client_class = MagicMock()
|
||||
self.mock_client = self.mock_client_class.return_value
|
||||
|
||||
def tearDown(self):
|
||||
UserDomainRole.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
UserPortfolioPermission.objects.all().delete()
|
||||
Suborganization.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_page_new_request(self):
|
||||
"""Tests that the requesting entity page loads correctly when a new request is started"""
|
||||
|
||||
response = self.app.get(reverse("domain-request:"))
|
||||
|
||||
# Navigate past the intro page
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
intro_form = response.forms[0]
|
||||
response = intro_form.submit().follow()
|
||||
|
||||
# Test the requesting entiy page
|
||||
self.assertContains(response, "Who will use the domain you’re requesting?")
|
||||
self.assertContains(response, "Add suborganization information")
|
||||
# We expect to see the portfolio name in two places:
|
||||
# the header, and as one of the radio button options.
|
||||
self.assertContains(response, self.portfolio.organization_name, count=2)
|
||||
|
||||
# We expect the dropdown list to contain the suborganizations that currently exist on this portfolio
|
||||
self.assertContains(response, self.suborganization.name, count=1)
|
||||
self.assertContains(response, self.suborganization_2.name, count=1)
|
||||
|
||||
# However, we should only see suborgs that are on the actual portfolio
|
||||
self.assertNotContains(response, self.unrelated_suborganization.name)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_page_existing_suborg_submission(self):
|
||||
"""Tests that you can submit a form on this page and set a suborg"""
|
||||
response = self.app.get(reverse("domain-request:"))
|
||||
|
||||
# Navigate past the intro page
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
form = response.forms[0]
|
||||
response = form.submit().follow()
|
||||
|
||||
# Check that we're on the right page
|
||||
self.assertContains(response, "Who will use the domain you’re requesting?")
|
||||
form = response.forms[0]
|
||||
|
||||
# Test selecting an existing suborg
|
||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||
form["portfolio_requesting_entity-sub_organization"] = f"{self.suborganization.id}"
|
||||
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = False
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
response = form.submit().follow()
|
||||
|
||||
# Ensure that the post occurred successfully by checking that we're on the following page.
|
||||
self.assertContains(response, "Current websites")
|
||||
created_domain_request_exists = DomainRequest.objects.filter(
|
||||
organization_name__isnull=True, sub_organization=self.suborganization
|
||||
).exists()
|
||||
self.assertTrue(created_domain_request_exists)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_page_new_suborg_submission(self):
|
||||
"""Tests that you can submit a form on this page and set a new suborg"""
|
||||
response = self.app.get(reverse("domain-request:"))
|
||||
|
||||
# Navigate past the intro page
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
form = response.forms[0]
|
||||
response = form.submit().follow()
|
||||
|
||||
# Check that we're on the right page
|
||||
self.assertContains(response, "Who will use the domain you’re requesting?")
|
||||
form = response.forms[0]
|
||||
|
||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
||||
form["portfolio_requesting_entity-sub_organization"] = ""
|
||||
|
||||
form["portfolio_requesting_entity-requested_suborganization"] = "moon"
|
||||
form["portfolio_requesting_entity-suborganization_city"] = "kepler"
|
||||
form["portfolio_requesting_entity-suborganization_state_territory"] = "AL"
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
response = form.submit().follow()
|
||||
|
||||
# Ensure that the post occurred successfully by checking that we're on the following page.
|
||||
self.assertContains(response, "Current websites")
|
||||
created_domain_request_exists = DomainRequest.objects.filter(
|
||||
organization_name__isnull=True,
|
||||
sub_organization__isnull=True,
|
||||
requested_suborganization="moon",
|
||||
suborganization_city="kepler",
|
||||
suborganization_state_territory=DomainRequest.StateTerritoryChoices.ALABAMA,
|
||||
).exists()
|
||||
self.assertTrue(created_domain_request_exists)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_page_organization_submission(self):
|
||||
"""Tests submitting an organization on the requesting org form"""
|
||||
response = self.app.get(reverse("domain-request:"))
|
||||
|
||||
# Navigate past the intro page
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
form = response.forms[0]
|
||||
response = form.submit().follow()
|
||||
|
||||
# Check that we're on the right page
|
||||
self.assertContains(response, "Who will use the domain you’re requesting?")
|
||||
form = response.forms[0]
|
||||
|
||||
# Test selecting an existing suborg
|
||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = False
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
response = form.submit().follow()
|
||||
|
||||
# Ensure that the post occurred successfully by checking that we're on the following page.
|
||||
self.assertContains(response, "Current websites")
|
||||
created_domain_request_exists = DomainRequest.objects.filter(
|
||||
organization_name=self.portfolio.organization_name,
|
||||
).exists()
|
||||
self.assertTrue(created_domain_request_exists)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_page_errors(self):
|
||||
"""Tests that we get the expected form errors on requesting entity"""
|
||||
domain_request = completed_domain_request(user=self.user, portfolio=self.portfolio)
|
||||
response = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
||||
form = response.forms[0]
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Test missing suborganization selection
|
||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||
form["portfolio_requesting_entity-sub_organization"] = ""
|
||||
|
||||
response = form.submit()
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
self.assertContains(response, "Suborganization is required.", status_code=200)
|
||||
|
||||
# Test missing custom suborganization details
|
||||
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
||||
response = form.submit()
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
self.assertContains(response, "Requested suborganization is required.", status_code=200)
|
||||
self.assertContains(response, "City is required.", status_code=200)
|
||||
self.assertContains(response, "State, territory, or military post is required.", status_code=200)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@boto3_mocking.patching
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_submission_email_sent(self):
|
||||
"""Tests that an email is sent out on successful form submission"""
|
||||
AllowedEmail.objects.create(email=self.user.email)
|
||||
domain_request = completed_domain_request(
|
||||
user=self.user,
|
||||
# This is the additional details field
|
||||
has_anything_else=True,
|
||||
)
|
||||
domain_request.portfolio = self.portfolio
|
||||
domain_request.requested_suborganization = "moon"
|
||||
domain_request.suborganization_city = "kepler"
|
||||
domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA
|
||||
domain_request.save()
|
||||
domain_request.refresh_from_db()
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
|
||||
self.assertNotIn("Anything else", body)
|
||||
self.assertIn("kepler, AL", body)
|
||||
self.assertIn("Requesting entity:", body)
|
||||
self.assertIn("Administrators from your organization:", body)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@boto3_mocking.patching
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_viewonly(self):
|
||||
"""Tests the review steps page on under our viewonly context"""
|
||||
domain_request = completed_domain_request(
|
||||
user=create_test_user(),
|
||||
# This is the additional details field
|
||||
has_anything_else=True,
|
||||
)
|
||||
domain_request.portfolio = self.portfolio
|
||||
domain_request.requested_suborganization = "moon"
|
||||
domain_request.suborganization_city = "kepler"
|
||||
domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA
|
||||
domain_request.save()
|
||||
domain_request.refresh_from_db()
|
||||
|
||||
domain_request.submit()
|
||||
|
||||
response = self.app.get(reverse("domain-request-status-viewonly", kwargs={"pk": domain_request.pk}))
|
||||
self.assertContains(response, "Requesting entity")
|
||||
self.assertContains(response, "moon")
|
||||
self.assertContains(response, "kepler, AL")
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@boto3_mocking.patching
|
||||
@less_console_noise_decorator
|
||||
def test_requesting_entity_manage(self):
|
||||
"""Tests the review steps page on under our manage context"""
|
||||
domain_request = completed_domain_request(
|
||||
user=self.user,
|
||||
# This is the additional details field
|
||||
has_anything_else=True,
|
||||
)
|
||||
domain_request.portfolio = self.portfolio
|
||||
domain_request.requested_suborganization = "moon"
|
||||
domain_request.suborganization_city = "kepler"
|
||||
domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.ALABAMA
|
||||
domain_request.save()
|
||||
domain_request.refresh_from_db()
|
||||
|
||||
domain_request.submit()
|
||||
|
||||
response = self.app.get(reverse("domain-request-status", kwargs={"pk": domain_request.pk}))
|
||||
self.assertContains(response, "Requesting entity")
|
||||
self.assertContains(response, "moon")
|
||||
self.assertContains(response, "kepler, AL")
|
||||
|
|
|
@ -2887,8 +2887,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Status:")
|
||||
# click the "Withdraw request" button
|
||||
mock_client = MockSESClient()
|
||||
|
|
|
@ -137,7 +137,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
}
|
||||
|
||||
PORTFOLIO_UNLOCKING_STEPS = {
|
||||
PortfolioDomainRequestStep.REQUESTING_ENTITY: lambda self: self.domain_request.organization_name is not None,
|
||||
PortfolioDomainRequestStep.REQUESTING_ENTITY: lambda w: w.from_model("unlock_requesting_entity", False),
|
||||
PortfolioDomainRequestStep.CURRENT_SITES: lambda self: (
|
||||
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
|
||||
),
|
||||
|
@ -320,7 +320,15 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
# Clear context so the prop getter won't create a request here.
|
||||
# Creating a request will be handled in the post method for the
|
||||
# intro page.
|
||||
return render(request, "domain_request_intro.html", {"hide_requests": True, "hide_domains": True})
|
||||
return render(
|
||||
request,
|
||||
"domain_request_intro.html",
|
||||
{
|
||||
"hide_requests": True,
|
||||
"hide_domains": True,
|
||||
"hide_members": True,
|
||||
},
|
||||
)
|
||||
else:
|
||||
return self.goto(self.steps.first)
|
||||
|
||||
|
@ -580,7 +588,43 @@ class PortfolioDomainRequestWizard(DomainRequestWizard):
|
|||
# Portfolio pages
|
||||
class RequestingEntity(DomainRequestWizard):
|
||||
template_name = "domain_request_requesting_entity.html"
|
||||
forms = [forms.RequestingEntityForm]
|
||||
forms = [forms.RequestingEntityYesNoForm, forms.RequestingEntityForm]
|
||||
|
||||
def save(self, forms: list):
|
||||
"""Override of save to clear or associate certain suborganization data
|
||||
depending on what the user wishes to do. For instance, we want to add a suborganization
|
||||
if the user selects one."""
|
||||
|
||||
# Get the yes/no dropdown value
|
||||
yesno_form = forms[0]
|
||||
yesno_cleaned_data = yesno_form.cleaned_data
|
||||
requesting_entity_is_suborganization = yesno_cleaned_data.get("requesting_entity_is_suborganization")
|
||||
|
||||
# Get the suborg value, and the requested suborg value
|
||||
requesting_entity_form = forms[1]
|
||||
cleaned_data = requesting_entity_form.cleaned_data
|
||||
sub_organization = cleaned_data.get("sub_organization")
|
||||
requested_suborganization = cleaned_data.get("requested_suborganization")
|
||||
|
||||
# Do some data cleanup, depending on what option was checked
|
||||
if requesting_entity_is_suborganization and (sub_organization or requested_suborganization):
|
||||
# Cleanup the organization name field, as this isn't for suborganizations.
|
||||
requesting_entity_form.cleaned_data.update({"organization_name": None})
|
||||
else:
|
||||
# If the user doesn't intend to create a suborg, simply don't make one and do some data cleanup
|
||||
requesting_entity_form.cleaned_data.update(
|
||||
{
|
||||
"organization_name": (
|
||||
self.domain_request.portfolio.organization_name if self.domain_request.portfolio else None
|
||||
),
|
||||
"sub_organization": None,
|
||||
"requested_suborganization": None,
|
||||
"suborganization_city": None,
|
||||
"suborganization_state_territory": None,
|
||||
}
|
||||
)
|
||||
|
||||
super().save(forms)
|
||||
|
||||
|
||||
class PortfolioAdditionalDetails(DomainRequestWizard):
|
||||
|
@ -826,6 +870,18 @@ class DomainRequestStatus(DomainRequestPermissionView):
|
|||
|
||||
return True
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Context override to add a step list to the context"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Create a temp wizard object to grab the step list
|
||||
if self.request.user.is_org_user(self.request):
|
||||
wizard = PortfolioDomainRequestWizard()
|
||||
wizard.request = self.request
|
||||
context["Step"] = PortfolioDomainRequestStep.__members__
|
||||
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
|
||||
context["form_titles"] = wizard.titles
|
||||
return context
|
||||
|
||||
|
||||
class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView):
|
||||
"""This page will ask user to confirm if they want to withdraw
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue