mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-28 16:29:54 +02:00
Merge remote-tracking branch 'origin/main' into nl/2907-search-screenreader-output
This commit is contained in:
commit
051736dd2f
31 changed files with 1702 additions and 195 deletions
57
.github/workflows/clone-staging.yaml
vendored
57
.github/workflows/clone-staging.yaml
vendored
|
@ -18,35 +18,30 @@ jobs:
|
|||
clone-database:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CF_USERNAME: CF_MS_USERNAME
|
||||
CF_PASSWORD: CF_MS_PASSWORD
|
||||
CF_USERNAME: ${{ secrets[env.CF_USERNAME] }}
|
||||
CF_PASSWORD: ${{ secrets[env.CF_PASSWORD] }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Share DB Service
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ env.DESTINATION_ENVIRONMENT }}
|
||||
cf_command: share-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }}
|
||||
|
||||
- name: Clone Database
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets.CF_MS_USERNAME }}
|
||||
cf_password: ${{ secrets.CF_MS_PASSWORD }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ env.SOURCE_ENVIRONMENT }}
|
||||
command: cg-manage-rds clone getgov-${{ env.SOURCE_ENVIRONMENT }}-database getgov-${{ env.DESTINATION_ENVIRONMENT }}-database
|
||||
|
||||
- name: Unshare DB Service
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets.CF_MS_USERNAME }}
|
||||
cf_password: ${{ secrets.CF_MS_PASSWORD }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: ${{ env.SOURCE_ENVIRONMENT }}
|
||||
cf_command: unshare-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }}
|
||||
- name: Clone Database
|
||||
run: |
|
||||
# install cf cli and other tools
|
||||
wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo gpg --dearmor -o /usr/share/keyrings/cli.cloudfoundry.org.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cli.cloudfoundry.org.gpg] https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install cf8-cli postgresql-client
|
||||
# install cg-manage-rds tool
|
||||
pip install git+https://github.com/cloud-gov/cg-manage-rds.git
|
||||
|
||||
# Authenticate and target CF org and space.
|
||||
cf api api.fr.cloud.gov
|
||||
cf auth $CF_USERNAME $CF_PASSWORD
|
||||
cf target -o cisa-dotgov -s $DESTINATION_ENVIRONMENT
|
||||
|
||||
# share the target db with the source space
|
||||
cf share-service getgov-$DESTINATION_ENVIRONMENT-database -s $SOURCE_ENVIRONMENT
|
||||
|
||||
# clone from source to destination
|
||||
cg-manage-rds clone getgov-$DESTINATION_ENVIRONMENT-database getgov-$SOURCE_ENVIRONMENT-database
|
||||
|
||||
# unshare the service
|
||||
cf unshare-service getgov-$DESTINATION_ENVIRONMENT-database -s $SOURCE_ENVIRONMENT
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
"http://localhost:8080/request/anything_else/",
|
||||
"http://localhost:8080/request/requirements/",
|
||||
"http://localhost:8080/request/finished/",
|
||||
"http://localhost:8080/user-profile/"
|
||||
"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.
|
||||
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
|
||||
# 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");
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -26,8 +26,8 @@ const hideElement = (element) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Show element
|
||||
*
|
||||
* Show element
|
||||
*
|
||||
*/
|
||||
const showElement = (element) => {
|
||||
element.classList.remove('display-none');
|
||||
|
@ -297,28 +297,56 @@ function clearValidators(el) {
|
|||
* radio button is false (hides this element if true)
|
||||
* **/
|
||||
function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) {
|
||||
HookupRadioTogglerListener(radioButtonName, {
|
||||
'True': elementIdToShowIfYes,
|
||||
'False': elementIdToShowIfNo
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookup listeners for radio togglers in form fields.
|
||||
*
|
||||
* Parameters:
|
||||
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
|
||||
* - valueToElementMap: An object where keys are the values of the radio buttons,
|
||||
* and values are the corresponding DOM element IDs to show. All other elements will be hidden.
|
||||
*
|
||||
* Usage Example:
|
||||
* Assuming you have radio buttons with values 'option1', 'option2', and 'option3',
|
||||
* and corresponding DOM IDs 'section1', 'section2', 'section3'.
|
||||
*
|
||||
* HookupValueBasedListener('exampleRadioGroup', {
|
||||
* 'option1': 'section1',
|
||||
* 'option2': 'section2',
|
||||
* 'option3': 'section3'
|
||||
* });
|
||||
**/
|
||||
function HookupRadioTogglerListener(radioButtonName, valueToElementMap) {
|
||||
// Get the radio buttons
|
||||
let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]');
|
||||
|
||||
// Extract the list of all element IDs from the valueToElementMap
|
||||
let allElementIds = Object.values(valueToElementMap);
|
||||
|
||||
function handleRadioButtonChange() {
|
||||
// Check the value of the selected radio button
|
||||
// Attempt to find the radio button element that is checked
|
||||
// Find the checked radio button
|
||||
let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked');
|
||||
|
||||
// Check if the element exists before accessing its value
|
||||
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
||||
|
||||
switch (selectedValue) {
|
||||
case 'True':
|
||||
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 1);
|
||||
break;
|
||||
// Hide all elements by default
|
||||
allElementIds.forEach(function (elementId) {
|
||||
let element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
hideElement(element);
|
||||
}
|
||||
});
|
||||
|
||||
case 'False':
|
||||
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 2);
|
||||
break;
|
||||
|
||||
default:
|
||||
toggleTwoDomElements(elementIdToShowIfYes, elementIdToShowIfNo, 0);
|
||||
// Show the relevant element for the selected value
|
||||
if (selectedValue && valueToElementMap[selectedValue]) {
|
||||
let elementToShow = document.getElementById(valueToElementMap[selectedValue]);
|
||||
if (elementToShow) {
|
||||
showElement(elementToShow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,11 +356,12 @@ function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToS
|
|||
radioButton.addEventListener('change', handleRadioButtonChange);
|
||||
});
|
||||
|
||||
// initialize
|
||||
// Initialize by checking the current state
|
||||
handleRadioButtonChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle
|
||||
function toggleTwoDomElements(ele1, ele2, index) {
|
||||
let element1 = document.getElementById(ele1);
|
||||
|
@ -912,6 +941,18 @@ function setupUrbanizationToggle(stateTerritoryField) {
|
|||
HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null)
|
||||
})();
|
||||
|
||||
|
||||
/**
|
||||
* An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly
|
||||
*
|
||||
*/
|
||||
(function newMemberFormListener() {
|
||||
HookupRadioTogglerListener('member_access_level', {
|
||||
'admin': 'new-member-admin-permissions',
|
||||
'basic': 'new-member-basic-permissions'
|
||||
});
|
||||
})();
|
||||
|
||||
/**
|
||||
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
|
||||
*
|
||||
|
@ -2734,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());
|
||||
})();
|
||||
|
|
|
@ -120,6 +120,11 @@ urlpatterns = [
|
|||
# views.PortfolioNoMembersView.as_view(),
|
||||
# name="no-portfolio-members",
|
||||
# ),
|
||||
path(
|
||||
"members/new-member/",
|
||||
views.NewMemberView.as_view(),
|
||||
name="new-member",
|
||||
),
|
||||
path(
|
||||
"requests/",
|
||||
views.PortfolioDomainRequestsView.as_view(),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import logging
|
||||
from django import forms
|
||||
from django.core.validators import RegexValidator
|
||||
from django.core.validators import MaxLengthValidator
|
||||
|
||||
from registrar.models import (
|
||||
PortfolioInvitation,
|
||||
|
@ -10,6 +11,7 @@ from registrar.models import (
|
|||
DomainInformation,
|
||||
Portfolio,
|
||||
SeniorOfficial,
|
||||
User,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
|
||||
|
@ -160,3 +162,112 @@ class PortfolioInvitedMemberForm(forms.ModelForm):
|
|||
"roles",
|
||||
"additional_permissions",
|
||||
]
|
||||
|
||||
|
||||
class NewMemberForm(forms.ModelForm):
|
||||
member_access_level = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
|
||||
widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Member access level is required",
|
||||
},
|
||||
)
|
||||
admin_org_domain_request_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Admin domain request permission is required",
|
||||
},
|
||||
)
|
||||
admin_org_members_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Admin member permission is required",
|
||||
},
|
||||
)
|
||||
basic_org_domain_request_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[
|
||||
("view_only", "View all requests"),
|
||||
("view_and_create", "View all requests plus create requests"),
|
||||
("no_access", "No access"),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Basic member permission is required",
|
||||
},
|
||||
)
|
||||
|
||||
email = forms.EmailField(
|
||||
label="Enter the email of the member you'd like to invite",
|
||||
max_length=None,
|
||||
error_messages={
|
||||
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
||||
"required": ("Enter an email address in the required format, like name@example.com."),
|
||||
},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["email"]
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Lowercase the value of the 'email' field
|
||||
email_value = cleaned_data.get("email")
|
||||
if email_value:
|
||||
cleaned_data["email"] = email_value.lower()
|
||||
|
||||
##########################################
|
||||
# TODO: future ticket
|
||||
# (invite new member)
|
||||
##########################################
|
||||
# Check for an existing user (if there isn't any, send an invite)
|
||||
# if email_value:
|
||||
# try:
|
||||
# existingUser = User.objects.get(email=email_value)
|
||||
# except User.DoesNotExist:
|
||||
# raise forms.ValidationError("User with this email does not exist.")
|
||||
|
||||
member_access_level = cleaned_data.get("member_access_level")
|
||||
|
||||
# Intercept the error messages so that we don't validate hidden inputs
|
||||
if not member_access_level:
|
||||
# If no member access level has been selected, delete error messages
|
||||
# for all hidden inputs (which is everything except the e-mail input
|
||||
# and member access selection)
|
||||
for field in self.fields:
|
||||
if field in self.errors and field != "email" and field != "member_access_level":
|
||||
del self.errors[field]
|
||||
return cleaned_data
|
||||
|
||||
basic_dom_req_error = "basic_org_domain_request_permissions"
|
||||
admin_dom_req_error = "admin_org_domain_request_permissions"
|
||||
admin_member_error = "admin_org_members_permissions"
|
||||
|
||||
if member_access_level == "admin" and basic_dom_req_error in self.errors:
|
||||
# remove the error messages pertaining to basic permission inputs
|
||||
del self.errors[basic_dom_req_error]
|
||||
elif member_access_level == "basic":
|
||||
# remove the error messages pertaining to admin permission inputs
|
||||
if admin_dom_req_error in self.errors:
|
||||
del self.errors[admin_dom_req_error]
|
||||
if admin_member_error in self.errors:
|
||||
del self.errors[admin_member_error]
|
||||
return cleaned_data
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -425,3 +425,70 @@ class DomainInformation(TimeStampedModel):
|
|||
return self.domain.get_state_display()
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_organization_name(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.organization_name
|
||||
return self.organization_name
|
||||
|
||||
# ----- Portfolio Properties -----
|
||||
@property
|
||||
def converted_generic_org_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.organization_type
|
||||
return self.generic_org_type
|
||||
|
||||
@property
|
||||
def converted_federal_agency(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_agency
|
||||
return self.federal_agency
|
||||
|
||||
@property
|
||||
def converted_federal_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type
|
||||
return self.federal_type
|
||||
|
||||
@property
|
||||
def converted_senior_official(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.senior_official
|
||||
return self.senior_official
|
||||
|
||||
@property
|
||||
def converted_address_line1(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.address_line1
|
||||
return self.address_line1
|
||||
|
||||
@property
|
||||
def converted_address_line2(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.address_line2
|
||||
return self.address_line2
|
||||
|
||||
@property
|
||||
def converted_city(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.city
|
||||
return self.city
|
||||
|
||||
@property
|
||||
def converted_state_territory(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.state_territory
|
||||
return self.state_territory
|
||||
|
||||
@property
|
||||
def converted_zipcode(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.zipcode
|
||||
return self.zipcode
|
||||
|
||||
@property
|
||||
def converted_urbanization(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.urbanization
|
||||
return self.urbanization
|
||||
|
|
|
@ -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,7 +1125,61 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
self.creator.restrict_user()
|
||||
|
||||
# ## Form policies ###
|
||||
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
|
||||
# during the domain request flow. They are policies about the domain request so
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
{# hint: spacing in the class string matters #}
|
||||
class="{{ uswds_input_class }}{% if classes %} {{ classes }}{% endif %}"
|
||||
{% if widget.value != None %}value="{{ widget.value|stringformat:'s' }}"{% endif %}
|
||||
{% if aria_label %}aria-label="{{ aria_label }} {{ label }}"{% endif %}
|
||||
{% if sublabel_text %}aria-describedby="{{ widget.attrs.id }}__sublabel"{% endif %}
|
||||
{% include "django/forms/widgets/attrs.html" %}
|
||||
/>
|
||||
|
|
|
@ -63,11 +63,12 @@
|
|||
|
||||
<div class="grid-row margin-top-1">
|
||||
<div class="grid-col">
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record text-secondary line-height-sans-5">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>Delete
|
||||
</button>
|
||||
<button type="button" id="button label" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record text-secondary line-height-sans-5">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>Delete
|
||||
<span class="sr-only">DS data record {{forloop.counter}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-5">
|
||||
{% with sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% with label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
@ -56,6 +56,7 @@
|
|||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>Delete
|
||||
<span class="sr-only">Name server {{forloop.counter}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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,9 +93,9 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if has_organization_members_flag %}
|
||||
{% if has_organization_members_flag and not hide_members %}
|
||||
<li class="usa-nav__primary-item">
|
||||
<a href="/members/" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
|
||||
<a href="{% url 'members' %}" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
|
||||
Members
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -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%}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
|
|
|
@ -145,92 +145,96 @@
|
|||
{% endblock request_summary_header%}
|
||||
|
||||
{% block request_summary %}
|
||||
{% 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 %}
|
||||
{% endwith %}
|
||||
|
||||
{% if DomainRequest.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
||||
|
||||
{% if DomainRequest.federally_recognized_tribe %}
|
||||
<p>Federally-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.state_recognized_tribe %}
|
||||
<p>State-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.get_federal_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.is_election_board %}
|
||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.about_your_organization %}
|
||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.senior_official %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.requested_domain %}
|
||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.alternative_domains.all %}
|
||||
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.purpose %}
|
||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.creator %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% if portfolio %}
|
||||
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
{% endwith %}
|
||||
|
||||
{# We always show this field even if None #}
|
||||
{% if DomainRequest %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.cisa_representative_first_name %}
|
||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||
{% else %}
|
||||
No
|
||||
{% if DomainRequest.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
||||
|
||||
{% if DomainRequest.federally_recognized_tribe %}
|
||||
<p>Federally-recognized tribe</p>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.anything_else %}
|
||||
{{DomainRequest.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
|
||||
{% if DomainRequest.state_recognized_tribe %}
|
||||
<p>State-recognized tribe</p>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.get_federal_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.is_election_board %}
|
||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.about_your_organization %}
|
||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.senior_official %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.requested_domain %}
|
||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.alternative_domains.all %}
|
||||
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.purpose %}
|
||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.creator %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{# We always show this field even if None #}
|
||||
{% if DomainRequest %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.cisa_representative_first_name %}
|
||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.anything_else %}
|
||||
{{DomainRequest.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock request_summary%}
|
||||
</div>
|
||||
</main>
|
|
@ -21,7 +21,7 @@
|
|||
{% if has_edit_members_portfolio_permission %}
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
<p class="float-right-tablet tablet:margin-y-0">
|
||||
<a href="#" class="usa-button"
|
||||
<a href="{% url 'new-member' %}" class="usa-button"
|
||||
>
|
||||
Add a new member
|
||||
</a>
|
||||
|
|
117
src/registrar/templates/portfolio_members_add_new.html
Normal file
117
src/registrar/templates/portfolio_members_add_new.html
Normal file
|
@ -0,0 +1,117 @@
|
|||
{% extends 'portfolio_base.html' %}
|
||||
{% load static url_helpers %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block title %} Members | New Member {% endblock %}
|
||||
|
||||
{% block wrapper_class %}
|
||||
{{ block.super }} dashboard--grey-1
|
||||
{% endblock %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
<!-- Form mesages -->
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock messages%}
|
||||
|
||||
<!-- Navigation breadcrumbs -->
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'members' %}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Add a new member</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Page header -->
|
||||
{% block new_member_header %}
|
||||
<h1>Add a new member</h1>
|
||||
{% endblock new_member_header %}
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Email</h2>
|
||||
</legend>
|
||||
<!-- Member email -->
|
||||
{% csrf_token %}
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
</fieldset>
|
||||
|
||||
<!-- Member access radio buttons (Toggles other sections) -->
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Member Access</h2>
|
||||
</legend>
|
||||
|
||||
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
<div class="usa-radio">
|
||||
{% for radio in form.member_access_level %}
|
||||
{{ radio.tag }}
|
||||
<label class="usa-radio__label usa-legend" for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
<p class="margin-0 margin-top-2">
|
||||
{% if radio.choice_label == "Admin Access" %}
|
||||
Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately.
|
||||
{% else %}
|
||||
Grants this member access to the organization. They can be given extra permissions to view all organization domain requests and submit domain requests on behalf of the organization. Basic access members can’t view all members of an organization or manage them. Domain management can be assigned separately.
|
||||
{% endif %}
|
||||
</p>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
</fieldset>
|
||||
|
||||
<!-- Admin access form -->
|
||||
<div id="new-member-admin-permissions" class="margin-top-2">
|
||||
<h2>Admin access permissions</h2>
|
||||
<p>Member permissions available for admin-level acccess.</p>
|
||||
|
||||
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.admin_org_domain_request_permissions %}
|
||||
{% endwith %}
|
||||
|
||||
<h3 class="margin-bottom-0 margin-top-3">Organization members</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.admin_org_members_permissions %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- Basic access form -->
|
||||
<div id="new-member-basic-permissions" class="margin-top-2">
|
||||
<h2>Basic member permissions</h2>
|
||||
<p>Member permissions available for basic-level access</p>
|
||||
{% input_with_errors form.basic_org_domain_request_permissions %}
|
||||
</div>
|
||||
|
||||
<!-- Submit/cancel buttons -->
|
||||
<div class="margin-top-3">
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'members' %}"
|
||||
class="usa-button usa-button--outline"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Cancel adding new member"
|
||||
>Cancel
|
||||
</a>
|
||||
<button type="submit" class="usa-button">Invite Member</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock portfolio_content%}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -24,6 +24,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
add_label_class: append to input element's label's `class` attribute
|
||||
add_legend_class: append to input element's legend's `class` attribute
|
||||
add_group_class: append to input element's surrounding tag's `class` attribute
|
||||
add_aria_label: append to input element's `aria_label` attribute
|
||||
attr_* - adds or replaces any single html attribute for the input
|
||||
add_error_attr_* - like `attr_*` but only if field.errors is not empty
|
||||
toggleable_input: shows a simple edit button, and adds display-none to the input field.
|
||||
|
@ -55,6 +56,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
label_classes = []
|
||||
legend_classes = []
|
||||
group_classes = []
|
||||
aria_labels = []
|
||||
|
||||
# this will be converted to an attribute string
|
||||
described_by = []
|
||||
|
@ -98,6 +100,9 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
if "display-none" not in classes:
|
||||
classes.append("display-none")
|
||||
|
||||
elif key == "add_aria_label":
|
||||
aria_labels.append(value)
|
||||
|
||||
attrs["id"] = field.auto_id
|
||||
|
||||
# do some work for various edge cases
|
||||
|
@ -151,7 +156,10 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
# ensure we don't overwrite existing attribute value
|
||||
if "aria-describedby" in attrs:
|
||||
described_by.append(attrs["aria-describedby"])
|
||||
attrs["aria-describedby"] = " ".join(described_by)
|
||||
attrs["aria_describedby"] = " ".join(described_by)
|
||||
|
||||
if aria_labels:
|
||||
context["aria_label"] = " ".join(aria_labels)
|
||||
|
||||
# ask Django to give us the widget dict
|
||||
# see Widget.get_context() on
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
@ -319,7 +319,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)
|
||||
|
||||
|
@ -588,7 +596,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):
|
||||
|
@ -834,6 +878,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
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import logging
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
from registrar.forms.portfolio import (
|
||||
PortfolioInvitedMemberForm,
|
||||
PortfolioMemberForm,
|
||||
PortfolioOrgAddressForm,
|
||||
PortfolioSeniorOfficialForm,
|
||||
)
|
||||
from registrar.forms import portfolio as portfolioForms
|
||||
from registrar.models import Portfolio, User
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
|
@ -25,7 +20,6 @@ from registrar.views.utility.permission_views import (
|
|||
)
|
||||
from django.views.generic import View
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -51,15 +45,6 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
|||
return render(request, "portfolio_requests.html")
|
||||
|
||||
|
||||
class PortfolioMembersView(PortfolioMembersPermissionView, View):
|
||||
|
||||
template_name = "portfolio_members.html"
|
||||
|
||||
def get(self, request):
|
||||
"""Add additional context data to the template."""
|
||||
return render(request, "portfolio_members.html")
|
||||
|
||||
|
||||
class PortfolioMemberView(PortfolioMemberPermissionView, View):
|
||||
|
||||
template_name = "portfolio_member.html"
|
||||
|
@ -101,7 +86,7 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View):
|
|||
class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||
|
||||
template_name = "portfolio_member_permissions.html"
|
||||
form_class = PortfolioMemberForm
|
||||
form_class = portfolioForms.PortfolioMemberForm
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
|
@ -197,7 +182,7 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
|||
class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
|
||||
|
||||
template_name = "portfolio_member_permissions.html"
|
||||
form_class = PortfolioInvitedMemberForm
|
||||
form_class = portfolioForms.PortfolioInvitedMemberForm
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
|
@ -310,7 +295,7 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin):
|
|||
|
||||
model = Portfolio
|
||||
template_name = "portfolio_organization.html"
|
||||
form_class = PortfolioOrgAddressForm
|
||||
form_class = portfolioForms.PortfolioOrgAddressForm
|
||||
context_object_name = "portfolio"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -373,7 +358,7 @@ class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin):
|
|||
|
||||
model = Portfolio
|
||||
template_name = "portfolio_senior_official.html"
|
||||
form_class = PortfolioSeniorOfficialForm
|
||||
form_class = portfolioForms.PortfolioSeniorOfficialForm
|
||||
context_object_name = "portfolio"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
|
@ -394,3 +379,177 @@ class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin):
|
|||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
|
||||
class PortfolioMembersView(PortfolioMembersPermissionView, View):
|
||||
|
||||
template_name = "portfolio_members.html"
|
||||
|
||||
def get(self, request):
|
||||
"""Add additional context data to the template."""
|
||||
return render(request, "portfolio_members.html")
|
||||
|
||||
|
||||
class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||
|
||||
template_name = "portfolio_members_add_new.html"
|
||||
form_class = portfolioForms.NewMemberForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""Get the portfolio object based on the session."""
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if portfolio is None:
|
||||
raise Http404("No organization found for this user")
|
||||
return portfolio
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Include the instance in the form kwargs."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["instance"] = self.get_object()
|
||||
return kwargs
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests to display the form."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Handle POST requests to process form submission."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""Handle the case when the form is invalid."""
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to members table."""
|
||||
return reverse("members")
|
||||
|
||||
##########################################
|
||||
# TODO: future ticket #2854
|
||||
# (save/invite new member)
|
||||
##########################################
|
||||
|
||||
# def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True):
|
||||
# """Performs the sending of the member invitation email
|
||||
# email: string- email to send to
|
||||
# add_success: bool- default True indicates:
|
||||
# adding a success message to the view if the email sending succeeds
|
||||
|
||||
# raises EmailSendingError
|
||||
# """
|
||||
|
||||
# # Set a default email address to send to for staff
|
||||
# requestor_email = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# # Check if the email requestor has a valid email address
|
||||
# if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||
# requestor_email = requestor.email
|
||||
# elif not requestor.is_staff:
|
||||
# messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||
# logger.error(
|
||||
# f"Can't send email to '{email}' on domain '{self.object}'."
|
||||
# f"No email exists for the requestor '{requestor.username}'.",
|
||||
# exc_info=True,
|
||||
# )
|
||||
# return None
|
||||
|
||||
# # Check to see if an invite has already been sent
|
||||
# try:
|
||||
# invite = MemberInvitation.objects.get(email=email, domain=self.object)
|
||||
# # check if the invite has already been accepted
|
||||
# if invite.status == MemberInvitation.MemberInvitationStatus.RETRIEVED:
|
||||
# add_success = False
|
||||
# messages.warning(
|
||||
# self.request,
|
||||
# f"{email} is already a manager for this domain.",
|
||||
# )
|
||||
# else:
|
||||
# add_success = False
|
||||
# # else if it has been sent but not accepted
|
||||
# messages.warning(self.request, f"{email} has already been invited to this domain")
|
||||
# except Exception:
|
||||
# logger.error("An error occured")
|
||||
|
||||
# try:
|
||||
# send_templated_email(
|
||||
# "emails/member_invitation.txt",
|
||||
# "emails/member_invitation_subject.txt",
|
||||
# to_address=email,
|
||||
# context={
|
||||
# "portfolio": self.object,
|
||||
# "requestor_email": requestor_email,
|
||||
# },
|
||||
# )
|
||||
# except EmailSendingError as exc:
|
||||
# logger.warn(
|
||||
# "Could not sent email invitation to %s for domain %s",
|
||||
# email,
|
||||
# self.object,
|
||||
# exc_info=True,
|
||||
# )
|
||||
# raise EmailSendingError("Could not send email invitation.") from exc
|
||||
# else:
|
||||
# if add_success:
|
||||
# messages.success(self.request, f"{email} has been invited to this domain.")
|
||||
|
||||
# def _make_invitation(self, email_address: str, requestor: User):
|
||||
# """Make a Member invitation for this email and redirect with a message."""
|
||||
# try:
|
||||
# self._send_member_invitation_email(email=email_address, requestor=requestor)
|
||||
# except EmailSendingError:
|
||||
# messages.warning(self.request, "Could not send email invitation.")
|
||||
# else:
|
||||
# # (NOTE: only create a MemberInvitation if the e-mail sends correctly)
|
||||
# MemberInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
# return redirect(self.get_success_url())
|
||||
|
||||
# def form_valid(self, form):
|
||||
|
||||
# """Add the specified user as a member
|
||||
# for this portfolio.
|
||||
# Throws EmailSendingError."""
|
||||
# requested_email = form.cleaned_data["email"]
|
||||
# requestor = self.request.user
|
||||
# # look up a user with that email
|
||||
# try:
|
||||
# requested_user = User.objects.get(email=requested_email)
|
||||
# except User.DoesNotExist:
|
||||
# # no matching user, go make an invitation
|
||||
# return self._make_invitation(requested_email, requestor)
|
||||
# else:
|
||||
# # if user already exists then just send an email
|
||||
# try:
|
||||
# self._send_member_invitation_email(requested_email, requestor, add_success=False)
|
||||
# except EmailSendingError:
|
||||
# logger.warn(
|
||||
# "Could not send email invitation (EmailSendingError)",
|
||||
# self.object,
|
||||
# exc_info=True,
|
||||
# )
|
||||
# messages.warning(self.request, "Could not send email invitation.")
|
||||
# except Exception:
|
||||
# logger.warn(
|
||||
# "Could not send email invitation (Other Exception)",
|
||||
# self.object,
|
||||
# exc_info=True,
|
||||
# )
|
||||
# messages.warning(self.request, "Could not send email invitation.")
|
||||
|
||||
# try:
|
||||
# UserPortfolioPermission.objects.create(
|
||||
# user=requested_user,
|
||||
# portfolio=self.object,
|
||||
# role=UserDomainRole.Roles.MANAGER,
|
||||
# )
|
||||
# except IntegrityError:
|
||||
# messages.warning(self.request, f"{requested_email} is already a member of this portfolio")
|
||||
# else:
|
||||
# messages.success(self.request, f"Added user {requested_email}.")
|
||||
# return redirect(self.get_success_url())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue