mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-31 06:56:33 +02:00
Merge remote-tracking branch 'origin/main' into nl/3275-slowness-admin-tables
This commit is contained in:
commit
137399a115
58 changed files with 517 additions and 557 deletions
|
@ -29,6 +29,7 @@
|
||||||
* - tooltip dynamic content updated to include nested element (for better sizing control)
|
* - tooltip dynamic content updated to include nested element (for better sizing control)
|
||||||
* - modal exposed to window to be accessible in other js files
|
* - modal exposed to window to be accessible in other js files
|
||||||
* - fixed bug in createHeaderButton which added newlines to header button tooltips
|
* - fixed bug in createHeaderButton which added newlines to header button tooltips
|
||||||
|
* - modified combobox to handle error class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ("document" in window.self) {
|
if ("document" in window.self) {
|
||||||
|
@ -1213,6 +1214,11 @@ const enhanceComboBox = _comboBoxEl => {
|
||||||
input.setAttribute("class", INPUT_CLASS);
|
input.setAttribute("class", INPUT_CLASS);
|
||||||
input.setAttribute("type", "text");
|
input.setAttribute("type", "text");
|
||||||
input.setAttribute("role", "combobox");
|
input.setAttribute("role", "combobox");
|
||||||
|
// DOTGOV - handle error class for combobox
|
||||||
|
// Check if 'usa-input--error' exists in selectEl and add it to input if true
|
||||||
|
if (selectEl.classList.contains('usa-input--error')) {
|
||||||
|
input.classList.add('usa-input--error');
|
||||||
|
}
|
||||||
additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => {
|
additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => {
|
||||||
const value = Sanitizer.escapeHTML`${attr[key]}`;
|
const value = Sanitizer.escapeHTML`${attr[key]}`;
|
||||||
input.setAttribute(key, value);
|
input.setAttribute(key, value);
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { hideElement, showElement } from './helpers.js';
|
|
||||||
|
|
||||||
export function loadInitialValuesForComboBoxes() {
|
|
||||||
var overrideDefaultClearButton = true;
|
|
||||||
var isTyping = false;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
|
||||||
handleAllComboBoxElements();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleAllComboBoxElements() {
|
|
||||||
const comboBoxElements = document.querySelectorAll(".usa-combo-box");
|
|
||||||
comboBoxElements.forEach(comboBox => {
|
|
||||||
const input = comboBox.querySelector("input");
|
|
||||||
const select = comboBox.querySelector("select");
|
|
||||||
if (!input || !select) {
|
|
||||||
console.warn("No combobox element found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Set the initial value of the combobox
|
|
||||||
let initialValue = select.getAttribute("data-default-value");
|
|
||||||
let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
|
|
||||||
if (!clearInputButton) {
|
|
||||||
console.warn("No clear element found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override the default clear button behavior such that it no longer clears the input,
|
|
||||||
// it just resets to the data-initial-value.
|
|
||||||
// Due to the nature of how uswds works, this is slightly hacky.
|
|
||||||
// Use a MutationObserver to watch for changes in the dropdown list
|
|
||||||
const dropdownList = comboBox.querySelector(`#${input.id}--list`);
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
mutations.forEach(function(mutation) {
|
|
||||||
if (mutation.type === "childList") {
|
|
||||||
addBlankOption(clearInputButton, dropdownList, initialValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure the observer to watch for changes in the dropdown list
|
|
||||||
const config = { childList: true, subtree: true };
|
|
||||||
observer.observe(dropdownList, config);
|
|
||||||
|
|
||||||
// Input event listener to detect typing
|
|
||||||
input.addEventListener("input", () => {
|
|
||||||
isTyping = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Blur event listener to reset typing state
|
|
||||||
input.addEventListener("blur", () => {
|
|
||||||
isTyping = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hide the reset button when there is nothing to reset.
|
|
||||||
// Do this once on init, then everytime a change occurs.
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
select.addEventListener("change", () => {
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change the default input behaviour - have it reset to the data default instead
|
|
||||||
clearInputButton.addEventListener("click", (e) => {
|
|
||||||
if (overrideDefaultClearButton && initialValue) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
input.click();
|
|
||||||
// Find the dropdown option with the desired value
|
|
||||||
const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option");
|
|
||||||
if (dropdownOptions) {
|
|
||||||
dropdownOptions.forEach(option => {
|
|
||||||
if (option.getAttribute("data-value") === initialValue) {
|
|
||||||
// Simulate a click event on the dropdown option
|
|
||||||
option.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateClearButtonVisibility(select, initialValue, clearInputButton) {
|
|
||||||
if (select.value === initialValue) {
|
|
||||||
hideElement(clearInputButton);
|
|
||||||
}else {
|
|
||||||
showElement(clearInputButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBlankOption(clearInputButton, dropdownList, initialValue) {
|
|
||||||
if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) {
|
|
||||||
const blankOption = document.createElement("li");
|
|
||||||
blankOption.setAttribute("role", "option");
|
|
||||||
blankOption.setAttribute("data-value", "");
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option");
|
|
||||||
if (!initialValue){
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option--selected")
|
|
||||||
}
|
|
||||||
blankOption.textContent = "⎯";
|
|
||||||
|
|
||||||
dropdownList.insertBefore(blankOption, dropdownList.firstChild);
|
|
||||||
blankOption.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
overrideDefaultClearButton = false;
|
|
||||||
// Trigger the default clear behavior
|
|
||||||
clearInputButton.click();
|
|
||||||
overrideDefaultClearButton = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ import { initDomainValidators } from './domain-validators.js';
|
||||||
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
||||||
import { initializeUrbanizationToggle } from './urbanization.js';
|
import { initializeUrbanizationToggle } from './urbanization.js';
|
||||||
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
||||||
import { loadInitialValuesForComboBoxes } from './combobox.js';
|
|
||||||
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
||||||
import { initDomainsTable } from './table-domains.js';
|
import { initDomainsTable } from './table-domains.js';
|
||||||
import { initDomainRequestsTable } from './table-domain-requests.js';
|
import { initDomainRequestsTable } from './table-domain-requests.js';
|
||||||
|
@ -31,8 +30,6 @@ initializeUrbanizationToggle();
|
||||||
userProfileListener();
|
userProfileListener();
|
||||||
finishUserSetupListener();
|
finishUserSetupListener();
|
||||||
|
|
||||||
loadInitialValuesForComboBoxes();
|
|
||||||
|
|
||||||
handleRequestingEntityFieldset();
|
handleRequestingEntityFieldset();
|
||||||
|
|
||||||
initDomainsTable();
|
initDomainsTable();
|
||||||
|
|
|
@ -9,15 +9,15 @@ export function handleRequestingEntityFieldset() {
|
||||||
const formPrefix = "portfolio_requesting_entity";
|
const formPrefix = "portfolio_requesting_entity";
|
||||||
const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`);
|
const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`);
|
||||||
const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`);
|
const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`);
|
||||||
const select = document.getElementById(`id_${formPrefix}-sub_organization`);
|
const input = document.getElementById(`id_${formPrefix}-sub_organization`);
|
||||||
const selectParent = select?.parentElement;
|
const inputGrandParent = input?.parentElement?.parentElement;
|
||||||
|
const select = input?.previousElementSibling;
|
||||||
const suborgContainer = document.getElementById("suborganization-container");
|
const suborgContainer = document.getElementById("suborganization-container");
|
||||||
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
|
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
|
||||||
const suborgAddtlInstruction = document.getElementById("suborganization-addtl-instruction");
|
const suborgAddtlInstruction = document.getElementById("suborganization-addtl-instruction");
|
||||||
const subOrgCreateNewOption = document.getElementById("option-to-add-suborg")?.value;
|
|
||||||
// Make sure all crucial page elements exist before proceeding.
|
// Make sure all crucial page elements exist before proceeding.
|
||||||
// This more or less ensures that we are on the Requesting Entity page, and not elsewhere.
|
// This more or less ensures that we are on the Requesting Entity page, and not elsewhere.
|
||||||
if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return;
|
if (!radios || !input || !select || !inputGrandParent || !suborgContainer || !suborgDetailsContainer) return;
|
||||||
|
|
||||||
// requestingSuborganization: This just broadly determines if they're requesting a suborg at all
|
// 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.
|
// requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not.
|
||||||
|
@ -27,8 +27,8 @@ export function handleRequestingEntityFieldset() {
|
||||||
function toggleSuborganization(radio=null) {
|
function toggleSuborganization(radio=null) {
|
||||||
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
||||||
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
||||||
if (select.options.length == 2) { // --Select-- and other are the only options
|
if (select.options.length == 1) { // other is the only option
|
||||||
hideElement(selectParent); // Hide the select drop down and indicate requesting new suborg
|
hideElement(inputGrandParent); // Hide the combo box and indicate requesting new suborg
|
||||||
hideElement(suborgAddtlInstruction); // Hide additional instruction related to the list
|
hideElement(suborgAddtlInstruction); // Hide additional instruction related to the list
|
||||||
requestingNewSuborganization.value = "True";
|
requestingNewSuborganization.value = "True";
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,11 +37,6 @@ export function handleRequestingEntityFieldset() {
|
||||||
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
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(subOrgCreateNewOption, "other"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestingNewSuborganization.value === "True") {
|
if (requestingNewSuborganization.value === "True") {
|
||||||
select.value = "other";
|
select.value = "other";
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,7 +259,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Append unassigned domains section
|
// Append unassigned domains section
|
||||||
if (this.removedDomains.length) {
|
if (this.removedDomains.length) {
|
||||||
const unassignedHeader = document.createElement('h3');
|
const unassignedHeader = document.createElement('h3');
|
||||||
unassignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
unassignedHeader.classList.add('margin-bottom-1');
|
||||||
unassignedHeader.textContent = 'Unassigned domains';
|
unassignedHeader.textContent = 'Unassigned domains';
|
||||||
domainAssignmentSummary.appendChild(unassignedHeader);
|
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||||
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||||
|
@ -268,7 +268,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Append assigned domains section
|
// Append assigned domains section
|
||||||
if (this.addedDomains.length) {
|
if (this.addedDomains.length) {
|
||||||
const assignedHeader = document.createElement('h3');
|
const assignedHeader = document.createElement('h3');
|
||||||
assignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
assignedHeader.classList.add('margin-bottom-1');
|
||||||
assignedHeader.textContent = 'Assigned domains';
|
assignedHeader.textContent = 'Assigned domains';
|
||||||
domainAssignmentSummary.appendChild(assignedHeader);
|
domainAssignmentSummary.appendChild(assignedHeader);
|
||||||
domainAssignmentSummary.appendChild(assignedDomainsList);
|
domainAssignmentSummary.appendChild(assignedDomainsList);
|
||||||
|
@ -276,7 +276,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
|
|
||||||
// Append total assigned domains section
|
// Append total assigned domains section
|
||||||
const totalHeader = document.createElement('h3');
|
const totalHeader = document.createElement('h3');
|
||||||
totalHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
totalHeader.classList.add('margin-bottom-1');
|
||||||
totalHeader.textContent = 'Total assigned domains';
|
totalHeader.textContent = 'Total assigned domains';
|
||||||
domainAssignmentSummary.appendChild(totalHeader);
|
domainAssignmentSummary.appendChild(totalHeader);
|
||||||
const totalCount = document.createElement('p');
|
const totalCount = document.createElement('p');
|
||||||
|
|
|
@ -245,7 +245,7 @@ export class MembersTable extends BaseTable {
|
||||||
// Only generate HTML if the member has one or more assigned domains
|
// Only generate HTML if the member has one or more assigned domains
|
||||||
if (num_domains > 0) {
|
if (num_domains > 0) {
|
||||||
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||||
domainsHTML += "<h4 class='margin-y-0 text-primary'>Domains assigned</h4>";
|
domainsHTML += "<h4 class='margin-y-0'>Domains assigned</h4>";
|
||||||
domainsHTML += `<p class='margin-y-0'>This member is assigned to ${num_domains} domains:</p>`;
|
domainsHTML += `<p class='margin-y-0'>This member is assigned to ${num_domains} domains:</p>`;
|
||||||
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
||||||
|
|
||||||
|
@ -405,7 +405,7 @@ export class MembersTable extends BaseTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a permissions header and wrap the entire output in a container
|
// Add a permissions header and wrap the entire output in a container
|
||||||
permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0 text-primary'>Additional permissions for this member</h4>" + permissionsHTML + "</div>";
|
permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0'>Additional permissions for this member</h4>" + permissionsHTML + "</div>";
|
||||||
|
|
||||||
return permissionsHTML;
|
return permissionsHTML;
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,7 +188,7 @@ html[data-theme="dark"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
#branding h1,
|
#branding h1,
|
||||||
h1, h2, h3,
|
.dashboard h1, .dashboard h2, .dashboard h3,
|
||||||
.module h2 {
|
.module h2 {
|
||||||
font-weight: font-weight('bold');
|
font-weight: font-weight('bold');
|
||||||
}
|
}
|
||||||
|
@ -516,10 +516,6 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too
|
||||||
max-width: 68ex;
|
max-width: 68ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-summary-box__dhs-color {
|
|
||||||
color: $dhs-blue-70;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.dja-detail-table {
|
details.dja-detail-table {
|
||||||
display: inline-table;
|
display: inline-table;
|
||||||
background-color: var(--body-bg);
|
background-color: var(--body-bg);
|
||||||
|
@ -812,18 +808,6 @@ div.dja__model-description{
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
//-- Override some styling for the USWDS summary box (per design quidance for ticket #2055
|
|
||||||
.usa-summary-box {
|
|
||||||
background: #{$dhs-blue-10};
|
|
||||||
border-color: #{$dhs-blue-30};
|
|
||||||
max-width: 72ex;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usa-summary-box h3 {
|
|
||||||
color: #{$dhs-blue-60};
|
|
||||||
}
|
|
||||||
|
|
||||||
.module caption, .inline-group h2 {
|
.module caption, .inline-group h2 {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
@ -929,14 +913,6 @@ ul.add-list-reset {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain-name-wrap {
|
|
||||||
white-space: normal;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow: visible;
|
|
||||||
word-break: break-all;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.organization-admin-label {
|
.organization-admin-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: .8125rem;
|
font-size: .8125rem;
|
||||||
|
|
|
@ -59,7 +59,6 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: color('primary-dark');
|
|
||||||
margin-top: units(2);
|
margin-top: units(2);
|
||||||
margin-bottom: units(2);
|
margin-bottom: units(2);
|
||||||
}
|
}
|
||||||
|
@ -130,16 +129,6 @@ grid column to the max-width of the searchbar, which was calculated to be 33rem.
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotgov-status-box {
|
|
||||||
background-color: color('primary-lightest');
|
|
||||||
border-color: color('accent-cool-lighter');
|
|
||||||
}
|
|
||||||
|
|
||||||
.dotgov-status-box--action-need {
|
|
||||||
background-color: color('warning-lighter');
|
|
||||||
border-color: color('warning');
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
border-top: 1px solid color('primary-darker');
|
border-top: 1px solid color('primary-darker');
|
||||||
}
|
}
|
||||||
|
@ -228,14 +217,6 @@ abbr[title] {
|
||||||
max-width: 23ch;
|
max-width: 23ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis--30 {
|
|
||||||
max-width: 30ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis--50 {
|
|
||||||
max-width: 50ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-align-middle {
|
.vertical-align-middle {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -272,6 +253,14 @@ abbr[title] {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.string-wrap {
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: visible;
|
||||||
|
word-break: break-all;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
//Icon size adjustment used by buttons and form errors
|
//Icon size adjustment used by buttons and form errors
|
||||||
.usa-icon.usa-icon--large {
|
.usa-icon.usa-icon--large {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -285,4 +274,4 @@ abbr[title] {
|
||||||
|
|
||||||
.width-quarter {
|
.width-quarter {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,13 +236,6 @@ a.withdraw_outline:active {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotgov-table a
|
|
||||||
a .usa-icon,
|
|
||||||
.usa-button--with-icon .usa-icon {
|
|
||||||
height: 1.3em;
|
|
||||||
width: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Red, for delete buttons
|
// Red, for delete buttons
|
||||||
// Used on: All delete buttons
|
// Used on: All delete buttons
|
||||||
// Note: Can be simplified by adding text-secondary to delete anchors in tables
|
// Note: Can be simplified by adding text-secondary to delete anchors in tables
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
@use "cisa_colors" as *;
|
@use "cisa_colors" as *;
|
||||||
@use "typography" as *;
|
|
||||||
|
|
||||||
|
// Normalize typography in forms
|
||||||
|
.usa-form,
|
||||||
|
.usa-form fieldset {
|
||||||
|
font-size: 1rem;
|
||||||
|
.usa-legend {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
.usa-form .usa-button {
|
.usa-form .usa-button {
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
}
|
}
|
||||||
|
@ -69,16 +76,6 @@ legend.float-left-tablet + button.float-right-tablet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-only-label {
|
|
||||||
@extend .h4--sm-05;
|
|
||||||
font-weight: bold;
|
|
||||||
color: color('primary-dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-only-value {
|
|
||||||
margin-top: units(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-gray-1 .usa-radio {
|
.bg-gray-1 .usa-radio {
|
||||||
background: color('gray-1');
|
background: color('gray-1');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
@use "typography" as *;
|
|
||||||
|
|
||||||
.register-form-step > h1 {
|
.register-form-step > h1 {
|
||||||
//align to top of sidebar on first page of the form
|
//align to top of sidebar on first page of the form
|
||||||
|
@ -12,11 +11,7 @@
|
||||||
margin-top: units(1);
|
margin-top: units(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// header--body is used on the summary page and
|
.register-form-step h3:not(.margin-top-05) {
|
||||||
// should not be styled like the register form headers
|
|
||||||
.register-form-step h3 {
|
|
||||||
color: color('primary-dark');
|
|
||||||
letter-spacing: $letter-space--xs;
|
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
@ -64,26 +59,10 @@
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item hr,
|
.summary-item hr,
|
||||||
.review__step hr {
|
.review__step hr {
|
||||||
border: none; //reset
|
border: none; //reset
|
||||||
border-top: 1px solid color('primary-dark');
|
border-top: 1px solid color('primary-dark');
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: units(0.5);
|
margin-bottom: units(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.review__step__title a:visited {
|
|
||||||
color: color('primary');
|
|
||||||
}
|
|
||||||
|
|
||||||
.review__step__name {
|
|
||||||
color: color('primary-dark');
|
|
||||||
font-weight: font-weight('semibold');
|
|
||||||
margin-bottom: units(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.review__step__subheading {
|
|
||||||
color: color('primary-dark');
|
|
||||||
font-weight: font-weight('semibold');
|
|
||||||
margin-bottom: units(0.5);
|
|
||||||
}
|
|
||||||
|
|
15
src/registrar/assets/src/sass/_theme/_summary-box.scss
Normal file
15
src/registrar/assets/src/sass/_theme/_summary-box.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
|
.usa-summary-box {
|
||||||
|
background-color: color('primary-lightest');
|
||||||
|
border-color: color('accent-cool-lighter');
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-summary-box--action-needed {
|
||||||
|
background-color: color('warning-lighter');
|
||||||
|
border-color: color('warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-summary-box__heading {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -71,4 +71,4 @@
|
||||||
width: 70vw;
|
width: 70vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,41 +10,35 @@ address,
|
||||||
max-width: measure(5);
|
max-width: measure(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1:not(.usa-alert__heading),
|
||||||
|
h2:not(.usa-alert__heading),
|
||||||
|
h3:not(.usa-alert__heading),
|
||||||
|
h4:not(.usa-alert__heading),
|
||||||
|
h5:not(.usa-alert__heading),
|
||||||
|
h6:not(.usa-alert__heading) {
|
||||||
|
color: color('primary-darker');
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, .h1 {
|
||||||
|
font-size: 2.125rem;
|
||||||
@include typeset('sans', '2xl', 2);
|
@include typeset('sans', '2xl', 2);
|
||||||
margin: 0 0 units(2);
|
margin: 0 0 units(2);
|
||||||
color: color('primary-darker');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2, .h2 {
|
||||||
font-weight: font-weight('semibold');
|
line-height: 1.3;
|
||||||
line-height: line-height('heading', 3);
|
|
||||||
margin: units(4) 0 units(1);
|
margin: units(4) 0 units(1);
|
||||||
color: color('primary-darker');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header--body {
|
h3, .h3 {
|
||||||
margin-top: units(2);
|
font-size: 1.25rem;
|
||||||
font-weight: font-weight('semibold');
|
font-weight: font-weight('semibold');
|
||||||
// The units mixin can only get us close, so it's between
|
|
||||||
// hardcoding the value and using in markup
|
|
||||||
font-size: 16.96px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.h4--sm-05 {
|
h4, .h4 {
|
||||||
font-size: size('body', 'sm');
|
font-size: 1.125rem;
|
||||||
font-weight: normal;
|
line-height: 1.25;
|
||||||
color: color('primary');
|
font-weight: font-weight('semibold');
|
||||||
margin-bottom: units(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize typography in forms
|
|
||||||
.usa-form,
|
|
||||||
.usa-form fieldset {
|
|
||||||
font-size: 1rem;
|
|
||||||
.usa-legend {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.p--blockquote {
|
.p--blockquote {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
@forward "forms";
|
@forward "forms";
|
||||||
@forward "search";
|
@forward "search";
|
||||||
@forward "tooltips";
|
@forward "tooltips";
|
||||||
|
@forward "summary-box";
|
||||||
@forward "fieldsets";
|
@forward "fieldsets";
|
||||||
@forward "alerts";
|
@forward "alerts";
|
||||||
@forward "tables";
|
@forward "tables";
|
||||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.models import DomainRequest, FederalAgency
|
from registrar.models import DomainRequest, FederalAgency
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
from registrar.models.suborganization import Suborganization
|
from registrar.models.suborganization import Suborganization
|
||||||
|
@ -161,9 +162,10 @@ class DomainSuborganizationForm(forms.ModelForm):
|
||||||
"""Form for updating the suborganization"""
|
"""Form for updating the suborganization"""
|
||||||
|
|
||||||
sub_organization = forms.ModelChoiceField(
|
sub_organization = forms.ModelChoiceField(
|
||||||
|
label="Suborganization name",
|
||||||
queryset=Suborganization.objects.none(),
|
queryset=Suborganization.objects.none(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(),
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -178,20 +180,6 @@ class DomainSuborganizationForm(forms.ModelForm):
|
||||||
portfolio = self.instance.portfolio if self.instance else None
|
portfolio = self.instance.portfolio if self.instance else None
|
||||||
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
|
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
|
||||||
|
|
||||||
# Set initial value
|
|
||||||
if self.instance and self.instance.sub_organization:
|
|
||||||
self.fields["sub_organization"].initial = self.instance.sub_organization
|
|
||||||
|
|
||||||
# Set custom form label
|
|
||||||
self.fields["sub_organization"].label = "Suborganization name"
|
|
||||||
|
|
||||||
# Use the combobox rather than the regular select widget
|
|
||||||
self.fields["sub_organization"].widget.template_name = "django/forms/widgets/combobox.html"
|
|
||||||
|
|
||||||
# Set data-default-value attribute
|
|
||||||
if self.instance and self.instance.sub_organization:
|
|
||||||
self.fields["sub_organization"].widget.attrs["data-default-value"] = self.instance.sub_organization.pk
|
|
||||||
|
|
||||||
|
|
||||||
class BaseNameserverFormset(forms.BaseFormSet):
|
class BaseNameserverFormset(forms.BaseFormSet):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -456,6 +444,13 @@ class DomainSecurityEmailForm(forms.Form):
|
||||||
class DomainOrgNameAddressForm(forms.ModelForm):
|
class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
"""Form for updating the organization name and mailing address."""
|
"""Form for updating the organization name and mailing address."""
|
||||||
|
|
||||||
|
# for federal agencies we also want to know the top-level agency.
|
||||||
|
federal_agency = forms.ModelChoiceField(
|
||||||
|
label="Federal agency",
|
||||||
|
required=False,
|
||||||
|
queryset=FederalAgency.objects.all(),
|
||||||
|
widget=ComboboxWidget,
|
||||||
|
)
|
||||||
zipcode = forms.CharField(
|
zipcode = forms.CharField(
|
||||||
label="Zip code",
|
label="Zip code",
|
||||||
validators=[
|
validators=[
|
||||||
|
@ -469,6 +464,16 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
state_territory = forms.ChoiceField(
|
||||||
|
label="State, territory, or military post",
|
||||||
|
required=True,
|
||||||
|
choices=DomainInformation.StateTerritoryChoices.choices,
|
||||||
|
error_messages={
|
||||||
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
|
},
|
||||||
|
widget=ComboboxWidget(attrs={"required": True}),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DomainInformation
|
model = DomainInformation
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -486,25 +491,12 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
"organization_name": {"required": "Enter the name of your organization."},
|
"organization_name": {"required": "Enter the name of your organization."},
|
||||||
"address_line1": {"required": "Enter the street address of your organization."},
|
"address_line1": {"required": "Enter the street address of your organization."},
|
||||||
"city": {"required": "Enter the city where your organization is located."},
|
"city": {"required": "Enter the city where your organization is located."},
|
||||||
"state_territory": {
|
|
||||||
"required": "Select the state, territory, or military post where your organization is located."
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
# We need to set the required attributed for State/territory
|
|
||||||
# because for this fields we are creating an individual
|
|
||||||
# instance of the Select. For the other fields we use the for loop to set
|
|
||||||
# the class's required attribute to true.
|
|
||||||
"organization_name": forms.TextInput,
|
"organization_name": forms.TextInput,
|
||||||
"address_line1": forms.TextInput,
|
"address_line1": forms.TextInput,
|
||||||
"address_line2": forms.TextInput,
|
"address_line2": forms.TextInput,
|
||||||
"city": forms.TextInput,
|
"city": forms.TextInput,
|
||||||
"state_territory": forms.Select(
|
|
||||||
attrs={
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
choices=DomainInformation.StateTerritoryChoices.choices,
|
|
||||||
),
|
|
||||||
"urbanization": forms.TextInput,
|
"urbanization": forms.TextInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django import forms
|
||||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.forms.utility.wizard_form_helper import (
|
from registrar.forms.utility.wizard_form_helper import (
|
||||||
RegistrarForm,
|
RegistrarForm,
|
||||||
RegistrarFormSet,
|
RegistrarFormSet,
|
||||||
|
@ -43,7 +44,7 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
label="Suborganization name",
|
label="Suborganization name",
|
||||||
required=False,
|
required=False,
|
||||||
queryset=Suborganization.objects.none(),
|
queryset=Suborganization.objects.none(),
|
||||||
empty_label="--Select--",
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
requested_suborganization = forms.CharField(
|
requested_suborganization = forms.CharField(
|
||||||
label="Requested suborganization",
|
label="Requested suborganization",
|
||||||
|
@ -56,22 +57,44 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
suborganization_state_territory = forms.ChoiceField(
|
suborganization_state_territory = forms.ChoiceField(
|
||||||
label="State, territory, or military post",
|
label="State, territory, or military post",
|
||||||
required=False,
|
required=False,
|
||||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
choices=DomainRequest.StateTerritoryChoices.choices,
|
||||||
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Override of init to add the suborganization queryset"""
|
"""Override of init to add the suborganization queryset and 'other' option"""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.domain_request.portfolio:
|
if self.domain_request.portfolio:
|
||||||
self.fields["sub_organization"].queryset = Suborganization.objects.filter(
|
# Fetch the queryset for the portfolio
|
||||||
portfolio=self.domain_request.portfolio
|
queryset = Suborganization.objects.filter(portfolio=self.domain_request.portfolio)
|
||||||
)
|
# set the queryset appropriately so that post can validate against queryset
|
||||||
|
self.fields["sub_organization"].queryset = queryset
|
||||||
|
|
||||||
|
# Modify the choices to include "other" so that form can display options properly
|
||||||
|
self.fields["sub_organization"].choices = [(obj.id, str(obj)) for obj in queryset] + [
|
||||||
|
("other", "Other (enter your suborganization manually)")
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainRequest | Contact | None):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`.
|
||||||
|
Overrides RegistrarForm method in order to set sub_organization to 'other'
|
||||||
|
on GETs of the RequestingEntityForm."""
|
||||||
|
if obj is None:
|
||||||
|
return {}
|
||||||
|
# get the domain request as a dict, per usual method
|
||||||
|
domain_request_dict = {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||||
|
|
||||||
|
# set sub_organization to 'other' if is_requesting_new_suborganization is True
|
||||||
|
if isinstance(obj, DomainRequest) and obj.is_requesting_new_suborganization():
|
||||||
|
domain_request_dict["sub_organization"] = "other"
|
||||||
|
|
||||||
|
return domain_request_dict
|
||||||
|
|
||||||
def clean_sub_organization(self):
|
def clean_sub_organization(self):
|
||||||
"""On suborganization clean, set the suborganization value to None if the user is requesting
|
"""On suborganization clean, set the suborganization value to None if the user is requesting
|
||||||
a custom suborganization (as it doesn't exist yet)"""
|
a custom suborganization (as it doesn't exist yet)"""
|
||||||
|
|
||||||
# If it's a new suborganization, return None (equivalent to selecting nothing)
|
# If it's a new suborganization, return None (equivalent to selecting nothing)
|
||||||
if self.cleaned_data.get("is_requesting_new_suborganization"):
|
if self.cleaned_data.get("is_requesting_new_suborganization"):
|
||||||
return None
|
return None
|
||||||
|
@ -94,41 +117,60 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def full_clean(self):
|
def full_clean(self):
|
||||||
"""Validation logic to remove the custom suborganization value before clean is triggered.
|
"""Validation logic to temporarily remove the custom suborganization value before clean is triggered.
|
||||||
Without this override, the form will throw an 'invalid option' error."""
|
Without this override, the form will throw an 'invalid option' error."""
|
||||||
# Remove the custom other field before cleaning
|
# Ensure self.data is not None before proceeding
|
||||||
data = self.data.copy() if self.data else None
|
if self.data:
|
||||||
|
# handle case where form has been submitted
|
||||||
|
# Create a copy of the data for manipulation
|
||||||
|
data = self.data.copy()
|
||||||
|
|
||||||
# Remove the 'other' value from suborganization if it exists.
|
# Retrieve sub_organization and store in _original_suborganization
|
||||||
# This is a special value that tracks if the user is requesting a new suborg.
|
suborganization = data.get("portfolio_requesting_entity-sub_organization")
|
||||||
suborganization = self.data.get("portfolio_requesting_entity-sub_organization")
|
self._original_suborganization = suborganization
|
||||||
if suborganization and "other" in suborganization:
|
# If the original value was "other", clear it for validation
|
||||||
data["portfolio_requesting_entity-sub_organization"] = ""
|
if self._original_suborganization == "other":
|
||||||
|
data["portfolio_requesting_entity-sub_organization"] = ""
|
||||||
|
|
||||||
# Set the modified data back to the form
|
# Set the modified data back to the form
|
||||||
self.data = data
|
self.data = data
|
||||||
|
else:
|
||||||
|
# handle case of a GET
|
||||||
|
suborganization = None
|
||||||
|
if self.initial and "sub_organization" in self.initial:
|
||||||
|
suborganization = self.initial["sub_organization"]
|
||||||
|
|
||||||
|
# Check if is_requesting_new_suborganization is True
|
||||||
|
is_requesting_new_suborganization = False
|
||||||
|
if self.initial and "is_requesting_new_suborganization" in self.initial:
|
||||||
|
# Call the method if it exists
|
||||||
|
is_requesting_new_suborganization = self.initial["is_requesting_new_suborganization"]()
|
||||||
|
|
||||||
|
# Determine if "other" should be set
|
||||||
|
if is_requesting_new_suborganization and suborganization is None:
|
||||||
|
self._original_suborganization = "other"
|
||||||
|
else:
|
||||||
|
self._original_suborganization = suborganization
|
||||||
|
|
||||||
# Call the parent's full_clean method
|
# Call the parent's full_clean method
|
||||||
super().full_clean()
|
super().full_clean()
|
||||||
|
|
||||||
|
# Restore "other" if there are errors
|
||||||
|
if self.errors:
|
||||||
|
self.data["portfolio_requesting_entity-sub_organization"] = self._original_suborganization
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Custom clean implementation to handle our desired logic flow for suborganization.
|
"""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()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
# Do some custom error validation if the requesting entity is a suborg.
|
# Get the cleaned data
|
||||||
# Otherwise, just validate as normal.
|
suborganization = cleaned_data.get("sub_organization")
|
||||||
suborganization = self.cleaned_data.get("sub_organization")
|
is_requesting_new_suborganization = cleaned_data.get("is_requesting_new_suborganization")
|
||||||
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(
|
requesting_entity_is_suborganization = self.data.get(
|
||||||
"portfolio_requesting_entity-requesting_entity_is_suborganization"
|
"portfolio_requesting_entity-requesting_entity_is_suborganization"
|
||||||
)
|
)
|
||||||
if requesting_entity_is_suborganization == "True":
|
if requesting_entity_is_suborganization == "True":
|
||||||
if is_requesting_new_suborganization:
|
if is_requesting_new_suborganization:
|
||||||
# Validate custom suborganization fields
|
|
||||||
if not cleaned_data.get("requested_suborganization") and "requested_suborganization" not in self.errors:
|
if not cleaned_data.get("requested_suborganization") and "requested_suborganization" not in self.errors:
|
||||||
self.add_error("requested_suborganization", "Enter the name of your suborganization.")
|
self.add_error("requested_suborganization", "Enter the name of your suborganization.")
|
||||||
if not cleaned_data.get("suborganization_city"):
|
if not cleaned_data.get("suborganization_city"):
|
||||||
|
@ -141,6 +183,12 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
elif not suborganization:
|
elif not suborganization:
|
||||||
self.add_error("sub_organization", "Suborganization is required.")
|
self.add_error("sub_organization", "Suborganization is required.")
|
||||||
|
|
||||||
|
# If there are errors, restore the "other" value for rendering
|
||||||
|
if self.errors and getattr(self, "_original_suborganization", None) == "other":
|
||||||
|
self.cleaned_data["sub_organization"] = self._original_suborganization
|
||||||
|
elif not self.data and getattr(self, "_original_suborganization", None) == "other":
|
||||||
|
self.cleaned_data["sub_organization"] = self._original_suborganization
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -274,7 +322,7 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
# uncomment to see if modelChoiceField can be an arg later
|
# uncomment to see if modelChoiceField can be an arg later
|
||||||
required=False,
|
required=False,
|
||||||
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
|
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
|
||||||
empty_label="--Select--",
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
organization_name = forms.CharField(
|
organization_name = forms.CharField(
|
||||||
label="Organization name",
|
label="Organization name",
|
||||||
|
@ -294,10 +342,11 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
state_territory = forms.ChoiceField(
|
state_territory = forms.ChoiceField(
|
||||||
label="State, territory, or military post",
|
label="State, territory, or military post",
|
||||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
choices=DomainRequest.StateTerritoryChoices.choices,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": ("Select the state, territory, or military post where your organization is located.")
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
},
|
},
|
||||||
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
zipcode = forms.CharField(
|
zipcode = forms.CharField(
|
||||||
label="Zip code",
|
label="Zip code",
|
||||||
|
@ -413,6 +462,7 @@ class CurrentSitesForm(RegistrarForm):
|
||||||
error_messages={
|
error_messages={
|
||||||
"invalid": ("Enter your organization's current website in the required format, like example.com.")
|
"invalid": ("Enter your organization's current website in the required format, like example.com.")
|
||||||
},
|
},
|
||||||
|
widget=forms.URLInput(attrs={"aria-labelledby": "id_current_sites_header id_current_sites_body"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.core.validators import RegexValidator
|
||||||
from django.core.validators import MaxLengthValidator
|
from django.core.validators import MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
PortfolioInvitation,
|
PortfolioInvitation,
|
||||||
UserPortfolioPermission,
|
UserPortfolioPermission,
|
||||||
|
@ -33,6 +34,15 @@ class PortfolioOrgAddressForm(forms.ModelForm):
|
||||||
"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789.",
|
"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
state_territory = forms.ChoiceField(
|
||||||
|
label="State, territory, or military post",
|
||||||
|
required=True,
|
||||||
|
choices=DomainInformation.StateTerritoryChoices.choices,
|
||||||
|
error_messages={
|
||||||
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
|
},
|
||||||
|
widget=ComboboxWidget(attrs={"required": True}),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Portfolio
|
model = Portfolio
|
||||||
|
@ -47,25 +57,12 @@ class PortfolioOrgAddressForm(forms.ModelForm):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
"address_line1": {"required": "Enter the street address of your organization."},
|
"address_line1": {"required": "Enter the street address of your organization."},
|
||||||
"city": {"required": "Enter the city where your organization is located."},
|
"city": {"required": "Enter the city where your organization is located."},
|
||||||
"state_territory": {
|
|
||||||
"required": "Select the state, territory, or military post where your organization is located."
|
|
||||||
},
|
|
||||||
"zipcode": {"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."},
|
"zipcode": {"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."},
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
# We need to set the required attributed for State/territory
|
|
||||||
# because for this fields we are creating an individual
|
|
||||||
# instance of the Select. For the other fields we use the for loop to set
|
|
||||||
# the class's required attribute to true.
|
|
||||||
"address_line1": forms.TextInput,
|
"address_line1": forms.TextInput,
|
||||||
"address_line2": forms.TextInput,
|
"address_line2": forms.TextInput,
|
||||||
"city": forms.TextInput,
|
"city": forms.TextInput,
|
||||||
"state_territory": forms.Select(
|
|
||||||
attrs={
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
choices=DomainInformation.StateTerritoryChoices.choices,
|
|
||||||
),
|
|
||||||
# "urbanization": forms.TextInput,
|
# "urbanization": forms.TextInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
src/registrar/forms/utility/combobox.py
Normal file
5
src/registrar/forms/utility/combobox.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.forms import Select
|
||||||
|
|
||||||
|
|
||||||
|
class ComboboxWidget(Select):
|
||||||
|
template_name = "django/forms/widgets/combobox.html"
|
|
@ -4,9 +4,9 @@ import ipaddress
|
||||||
import re
|
import re
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from django.db import transaction
|
||||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||||
|
from django.db import models, IntegrityError
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from registrar.models.host import Host
|
from registrar.models.host import Host
|
||||||
|
@ -1329,14 +1329,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
def get_default_administrative_contact(self):
|
def get_default_administrative_contact(self):
|
||||||
"""Gets the default administrative contact."""
|
"""Gets the default administrative contact."""
|
||||||
logger.info("get_default_security_contact() -> Adding administrative security contact")
|
logger.info("get_default_administrative_contact() -> Adding default administrative contact")
|
||||||
contact = PublicContact.get_default_administrative()
|
contact = PublicContact.get_default_administrative()
|
||||||
contact.domain = self
|
contact.domain = self
|
||||||
return contact
|
return contact
|
||||||
|
|
||||||
def get_default_technical_contact(self):
|
def get_default_technical_contact(self):
|
||||||
"""Gets the default technical contact."""
|
"""Gets the default technical contact."""
|
||||||
logger.info("get_default_security_contact() -> Adding technical security contact")
|
logger.info("get_default_security_contact() -> Adding default technical contact")
|
||||||
contact = PublicContact.get_default_technical()
|
contact = PublicContact.get_default_technical()
|
||||||
contact.domain = self
|
contact.domain = self
|
||||||
return contact
|
return contact
|
||||||
|
@ -1678,9 +1678,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
for domainContact in contact_data:
|
for domainContact in contact_data:
|
||||||
req = commands.InfoContact(id=domainContact.contact)
|
req = commands.InfoContact(id=domainContact.contact)
|
||||||
data = registry.send(req, cleaned=True).res_data[0]
|
data = registry.send(req, cleaned=True).res_data[0]
|
||||||
|
logger.info(f"_fetch_contacts => this is the data: {data}")
|
||||||
|
|
||||||
# Map the object we recieved from EPP to a PublicContact
|
# Map the object we recieved from EPP to a PublicContact
|
||||||
mapped_object = self.map_epp_contact_to_public_contact(data, domainContact.contact, domainContact.type)
|
mapped_object = self.map_epp_contact_to_public_contact(data, domainContact.contact, domainContact.type)
|
||||||
|
logger.info(f"_fetch_contacts => mapped_object: {mapped_object}")
|
||||||
|
|
||||||
# Find/create it in the DB
|
# Find/create it in the DB
|
||||||
in_db = self._get_or_create_public_contact(mapped_object)
|
in_db = self._get_or_create_public_contact(mapped_object)
|
||||||
|
@ -1871,8 +1873,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
missingSecurity = True
|
missingSecurity = True
|
||||||
missingTech = True
|
missingTech = True
|
||||||
|
|
||||||
if len(cleaned.get("_contacts")) < 3:
|
contacts = cleaned.get("_contacts", [])
|
||||||
for contact in cleaned.get("_contacts"):
|
if len(contacts) < 3:
|
||||||
|
for contact in contacts:
|
||||||
if contact.type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
if contact.type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
||||||
missingAdmin = False
|
missingAdmin = False
|
||||||
if contact.type == PublicContact.ContactTypeChoices.SECURITY:
|
if contact.type == PublicContact.ContactTypeChoices.SECURITY:
|
||||||
|
@ -1891,6 +1894,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
technical_contact = self.get_default_technical_contact()
|
technical_contact = self.get_default_technical_contact()
|
||||||
technical_contact.save()
|
technical_contact.save()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"_add_missing_contacts_if_unknown => Adding contacts. Values are "
|
||||||
|
f"missingAdmin: {missingAdmin}, missingSecurity: {missingSecurity}, missingTech: {missingTech}"
|
||||||
|
)
|
||||||
|
|
||||||
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
||||||
"""Contact registry for info about a domain."""
|
"""Contact registry for info about a domain."""
|
||||||
try:
|
try:
|
||||||
|
@ -2104,8 +2112,21 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
# Save to DB if it doesn't exist already.
|
# Save to DB if it doesn't exist already.
|
||||||
if db_contact.count() == 0:
|
if db_contact.count() == 0:
|
||||||
# Doesn't run custom save logic, just saves to DB
|
# Doesn't run custom save logic, just saves to DB
|
||||||
public_contact.save(skip_epp_save=True)
|
try:
|
||||||
logger.info(f"Created a new PublicContact: {public_contact}")
|
with transaction.atomic():
|
||||||
|
public_contact.save(skip_epp_save=True)
|
||||||
|
logger.info(f"Created a new PublicContact: {public_contact}")
|
||||||
|
except IntegrityError as err:
|
||||||
|
logger.error(
|
||||||
|
f"_get_or_create_public_contact() => tried to create a duplicate public contact: {err}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return PublicContact.objects.get(
|
||||||
|
registry_id=public_contact.registry_id,
|
||||||
|
contact_type=public_contact.contact_type,
|
||||||
|
domain=self,
|
||||||
|
)
|
||||||
|
|
||||||
# Append the item we just created
|
# Append the item we just created
|
||||||
return public_contact
|
return public_contact
|
||||||
|
|
||||||
|
@ -2115,7 +2136,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
if existing_contact.email != public_contact.email or existing_contact.registry_id != public_contact.registry_id:
|
if existing_contact.email != public_contact.email or existing_contact.registry_id != public_contact.registry_id:
|
||||||
existing_contact.delete()
|
existing_contact.delete()
|
||||||
public_contact.save()
|
public_contact.save()
|
||||||
logger.warning("Requested PublicContact is out of sync " "with DB.")
|
logger.warning("Requested PublicContact is out of sync with DB.")
|
||||||
return public_contact
|
return public_contact
|
||||||
|
|
||||||
# If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
|
# If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
|
||||||
|
|
|
@ -15,9 +15,11 @@ class DomainHelper:
|
||||||
|
|
||||||
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
||||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||||
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")
|
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,200}(?<!-)\.[A-Za-z]{2,6}$")
|
||||||
|
|
||||||
# a domain can be no longer than 253 characters in total
|
# a domain can be no longer than 253 characters in total
|
||||||
|
# NOTE: the domain name is limited by the DOMAIN_REGEX above
|
||||||
|
# to 200 characters (not including the .gov at the end)
|
||||||
MAX_LENGTH = 253
|
MAX_LENGTH = 253
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
<dd>{{ current_user.email }}</dd>
|
<dd>{{ current_user.email }}</dd>
|
||||||
<dt>Phone:</dt>
|
<dt>Phone:</dt>
|
||||||
<dd>{{ current_user.phone }}</dd>
|
<dd>{{ current_user.phone }}</dd>
|
||||||
<h3 class="font-heading-md" aria-label="Data that will added to:"> </h3>
|
<h3 class="font-heading-md" aria-label="Data that will be added to:"> </h3>
|
||||||
<dt>Domains:</dt>
|
<dt>Domains:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if current_user_domains %}
|
{% if current_user_domains %}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
aria-labelledby="summary-box-description"
|
aria-labelledby="summary-box-description"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
<h3 class="usa-summary-box__heading" id="summary-box-description">
|
||||||
When a domain is deleted:
|
When a domain is deleted:
|
||||||
</h3>
|
</h3>
|
||||||
<div class="usa-summary-box__text">
|
<div class="usa-summary-box__text">
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
aria-labelledby="summary-box-description"
|
aria-labelledby="summary-box-description"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
<h3 class="usa-summary-box__heading">
|
||||||
When a domain is deleted:
|
When a domain is deleted:
|
||||||
</h3>
|
</h3>
|
||||||
<div class="usa-summary-box__text">
|
<div class="usa-summary-box__text">
|
||||||
|
|
|
@ -11,6 +11,7 @@ for now we just carry the attribute to both the parent element and the select.
|
||||||
{{ name }}="{{ value }}"
|
{{ name }}="{{ value }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
data-default-value="{% for group_name, group_choices, group_index in widget.optgroups %}{% for option in group_choices %}{% if option.selected %}{{ option.value }}{% endif %}{% endfor %}{% endfor %}"
|
||||||
>
|
>
|
||||||
{% include "django/forms/widgets/select.html" %}
|
{% include "django/forms/widgets/select.html" with is_combobox=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
{# hint: spacing in the class string matters #}
|
{# hint: spacing in the class string matters #}
|
||||||
class="usa-select{% if classes %} {{ classes }}{% endif %}"
|
class="usa-select{% if classes %} {{ classes }}{% endif %}"
|
||||||
{% include "django/forms/widgets/attrs.html" %}
|
{% include "django/forms/widgets/attrs.html" %}
|
||||||
|
{% if is_combobox %}
|
||||||
|
data-default-value="{% for group_name, group_choices, group_index in widget.optgroups %}{% for option in group_choices %}{% if option.selected %}{{ option.value }}{% endif %}{% endfor %}{% endfor %}"
|
||||||
|
{% endif %}
|
||||||
>
|
>
|
||||||
{% for group, options, index in widget.optgroups %}
|
{% for group, options, index in widget.optgroups %}
|
||||||
{% if group %}<optgroup label="{{ group }}">{% endif %}
|
{% if group %}<optgroup label="{{ group }}">{% endif %}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
|
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
|
||||||
<div class="tablet:grid-col-3 ">
|
<div class="tablet:grid-col-3 ">
|
||||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||||
text-primary-darker text-semibold domain-name-wrap"
|
text-primary-darker text-semibold string-wrap"
|
||||||
>
|
>
|
||||||
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
{% if not domain.domain_info %}
|
{% if not domain.domain_info %}
|
||||||
<div class="usa-alert usa-alert--error margin-bottom-2">
|
<div class="usa-alert usa-alert--error margin-bottom-2">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<h4 class="usa-alert__heading larger-font-sizing">Domain missing domain information</h4>
|
<h4 class="usa-alert__heading">Domain missing domain information</h4>
|
||||||
<p class="usa-alert__text ">
|
<p class="usa-alert__text ">
|
||||||
You are attempting to manage a domain, {{ domain.name }}, which does not have a domain information object. Please correct this in the admin by editing the domain, and adding domain information, as appropriate.
|
You are attempting to manage a domain, {{ domain.name }}, which does not have a domain information object. Please correct this in the admin by editing the domain, and adding domain information, as appropriate.
|
||||||
</p>
|
</p>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
{% if is_analyst_or_superuser and analyst_action == 'edit' and analyst_action_location == domain.pk %}
|
{% if is_analyst_or_superuser and analyst_action == 'edit' and analyst_action_location == domain.pk %}
|
||||||
<div class="usa-alert usa-alert--warning margin-bottom-2">
|
<div class="usa-alert usa-alert--warning margin-bottom-2">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<h4 class="usa-alert__heading larger-font-sizing">Attention!</h4>
|
<h4 class="usa-alert__heading">Attention!</h4>
|
||||||
<p class="usa-alert__text ">
|
<p class="usa-alert__text ">
|
||||||
You are making changes to a registrant’s domain. When finished making changes, close this tab and inform the registrant of your updates.
|
You are making changes to a registrant’s domain. When finished making changes, close this tab and inform the registrant of your updates.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -21,21 +21,17 @@
|
||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<div class="margin-top-4 tablet:grid-col-10">
|
<div class="margin-top-4 tablet:grid-col-10">
|
||||||
<h2 class="text-bold text-primary-dark domain-name-wrap">{{ domain.name }}</h2>
|
<h2 class="string-wrap">{{ domain.name }}</h2>
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box padding-bottom-0 margin-top-3 padding-left-2{% if not domain.is_expired %}{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} dotgov-status-box--action-need{% endif %}{% endif %}"
|
class="usa-summary-box padding-y-2 margin-bottom-1"
|
||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="summary-box-key-information"
|
aria-labelledby="summary-box-key-information"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
<div class="usa-summary-box__text padding-top-0"
|
||||||
id="summary-box-key-information"
|
|
||||||
>
|
>
|
||||||
<span class="text-bold text-primary-darker">
|
<p class="font-sans-md margin-top-0 margin-bottom-05 text-primary-darker">
|
||||||
Status:
|
<strong>Status:</strong>
|
||||||
</span>
|
|
||||||
<span class="text-primary-darker">
|
|
||||||
|
|
||||||
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
||||||
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
||||||
Expired
|
Expired
|
||||||
|
@ -46,9 +42,10 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.state|title }}
|
{{ domain.state|title }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</p>
|
||||||
|
|
||||||
{% if domain.get_state_help_text %}
|
{% if domain.get_state_help_text %}
|
||||||
<div class="padding-top-1 text-primary-darker">
|
<p class="margin-y-0 text-primary-darker">
|
||||||
{% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
|
{% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
|
||||||
This domain has expired, but it is still online.
|
This domain has expired, but it is still online.
|
||||||
{% url 'domain-renewal' pk=domain.id as url %}
|
{% url 'domain-renewal' pk=domain.id as url %}
|
||||||
|
@ -64,13 +61,11 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.get_state_help_text }}
|
{{ domain.get_state_help_text }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
|
|
||||||
|
|
||||||
{% include "includes/domain_dates.html" %}
|
{% include "includes/domain_dates.html" %}
|
||||||
|
|
||||||
|
|
|
@ -35,21 +35,23 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if has_dnssec_records %}
|
{% if has_dnssec_records %}
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box padding-top-0"
|
class="usa-summary-box "
|
||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="Important notes on disabling DNSSEC"
|
aria-labelledby="Important notes on disabling DNSSEC"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
<h2 class="usa-summary-box__heading"
|
||||||
id="summary-box-key-information"
|
>To fully disable DNSSEC</h2>
|
||||||
>
|
|
||||||
<h2>To fully disable DNSSEC </h2>
|
<div class="usa-summary-box__text">
|
||||||
<ul class="usa-list">
|
<ul class="usa-list">
|
||||||
<li>Click “Disable DNSSEC” below.</li>
|
<li>Click “Disable DNSSEC” below.</li>
|
||||||
<li>Wait until the Time to Live (TTL) expires on your DNSSEC records managed by your DNS hosting provider. This is often less than 24 hours, but confirm with your provider.</li>
|
<li>Wait until the Time to Live (TTL) expires on your DNSSEC records managed by your DNS hosting provider. This is often less than 24 hours, but confirm with your provider.</li>
|
||||||
<li>After the TTL expiration, disable DNSSEC at your DNS hosting provider. </li>
|
<li>After the TTL expiration, disable DNSSEC at your DNS hosting provider. </li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><strong>Warning:</strong> If you disable DNSSEC at your DNS hosting provider before TTL expiration, this may cause your domain to appear offline.</p>
|
<p><strong>Warning:</strong> If you disable DNSSEC at your DNS hosting provider before TTL expiration, this may cause your domain to appear offline.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2>DNSSEC is enabled on your domain</h2>
|
<h2>DNSSEC is enabled on your domain</h2>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<div class="margin-top-4 tablet:grid-col-10">
|
<div class="margin-top-4 tablet:grid-col-10">
|
||||||
<h2 class="text-bold text-primary-dark domain-name-wrap">Confirm the following information for accuracy</h2>
|
<h2 class="domain-name-wrap">Confirm the following information for accuracy</h2>
|
||||||
<p>Review these details below. We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" class="usa-link">
|
<p>Review these details below. We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" class="usa-link">
|
||||||
require</a> that you maintain accurate information for the domain.
|
require</a> that you maintain accurate information for the domain.
|
||||||
The details you provide will only be used to support the administration of .gov and won't be made public.
|
The details you provide will only be used to support the administration of .gov and won't be made public.
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p>We can better evaluate your request if we know about domains you’re already using.</p>
|
<p>We can better evaluate your request if we know about domains you’re already using.</p>
|
||||||
<h2>What are the current websites for your organization?</h2>
|
<h2 id="id_current_sites_header">What are the current websites for your organization?</h2>
|
||||||
<p>Enter your organization’s current public websites. If you already have a .gov domain, include that in your list. This question is optional.</p>
|
<p id="id_current_sites_body">Enter your organization’s current public websites. If you already have a .gov domain, include that in your list. This question is optional.</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_required_fields_help_text %}
|
{% block form_required_fields_help_text %}
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--with-icon usa-button--unstyled">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another site</span>
|
</svg><span class="margin-left-05">Add another site</span>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
<p id="domain_instructions" class="margin-top-05">After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all our requirements after you complete the rest of this form.</p>
|
<p id="domain_instructions" class="margin-top-05">After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all our requirements after you complete the rest of this form.</p>
|
||||||
|
|
||||||
{% with attr_aria_describedby="domain_instructions domain_instructions2" %}
|
{% with attr_aria_labelledby="domain_instructions domain_instructions2" attr_aria_describedby="id_dotgov_domain-requested_domain--toast" %}
|
||||||
{# attr_validate / validate="domain" invokes code in getgov.min.js #}
|
{# attr_validate / validate="domain" invokes code in getgov.min.js #}
|
||||||
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.requested_domain %}
|
{% input_with_errors forms.0.requested_domain %}
|
||||||
|
@ -67,18 +67,20 @@
|
||||||
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
||||||
you your first choice?</p>
|
you your first choice?</p>
|
||||||
|
|
||||||
{% with attr_aria_describedby="alt_domain_instructions" %}
|
{% with attr_aria_labelledby="alt_domain_instructions" %}
|
||||||
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
|
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
|
||||||
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
|
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
|
||||||
{% for form in forms.1 %}
|
{% for form in forms.1 %}
|
||||||
<div class="repeatable-form">
|
<div class="repeatable-form">
|
||||||
{% input_with_errors form.alternative_domain %}
|
{% with attr_aria_describedby=form.alternative_domain.auto_id|stringformat:"s"|add:"--toast" %}
|
||||||
|
{% input_with_errors form.alternative_domain %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<button type="button" value="save" class="usa-button usa-button--unstyled" id="add-form">
|
<button type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another alternative</span>
|
</svg><span class="margin-left-05">Add another alternative</span>
|
||||||
|
|
|
@ -31,13 +31,13 @@
|
||||||
<fieldset class="usa-fieldset repeatable-form padding-y-1">
|
<fieldset class="usa-fieldset repeatable-form padding-y-1">
|
||||||
|
|
||||||
<legend class="float-left-tablet">
|
<legend class="float-left-tablet">
|
||||||
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
<h3 class="margin-top-05">Organization contact {{ forloop.counter }}</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2 text-secondary line-height-sans-5">
|
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-top-1 text-secondary line-height-sans-5 usa-button--with-icon">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg><span class="margin-left-05">Delete</span>
|
</svg>Delete
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled" id="add-form">
|
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another contact</span>
|
</svg><span class="margin-left-05">Add another contact</span>
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
{% if domain_manager_roles %}
|
{% if domain_manager_roles %}
|
||||||
<section class="section-outlined" id="domain-managers">
|
<section class="section-outlined" id="domain-managers">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||||
<h2 class> Domain managers </h2>
|
<h2> Domain managers </h2>
|
||||||
<caption class="sr-only">Domain managers</caption>
|
<caption class="sr-only">Domain managers</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
></div>
|
></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a class="usa-button usa-button--unstyled" href="{% url 'domain-users-add' pk=domain.id %}">
|
<a class="usa-button usa-button--unstyled usa-button--with-icon" href="{% url 'domain-users-add' pk=domain.id %}">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add a domain manager</span>
|
</svg><span class="margin-left-05">Add a domain manager</span>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% if domain.expiration_date or domain.created_at %}
|
{% if domain.expiration_date or domain.created_at %}
|
||||||
<p class="margin-y-0">
|
<p>
|
||||||
{% if domain.expiration_date %}
|
{% if domain.expiration_date %}
|
||||||
<strong class="text-primary-dark">Expires:</strong>
|
<strong class="text-primary-dark">Expires:</strong>
|
||||||
{{ domain.expiration_date|date }}
|
{{ domain.expiration_date|date }}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{% load url_helpers %}
|
{% load url_helpers %}
|
||||||
|
|
||||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold" >
|
<h2>
|
||||||
Next steps in this process
|
Next steps in this process
|
||||||
</h2>
|
</h2>
|
||||||
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 30 business days. We’ll email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
|
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 30 business days. We’ll email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
|
||||||
|
|
||||||
{% if show_withdraw_text %}
|
{% if show_withdraw_text %}
|
||||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
|
<h2>
|
||||||
Need to make changes?
|
Need to make changes?
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ Template include for read-only form fields
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
<h4 class="read-only-label">{{ field.label }}</h4>
|
<h4 class="margin-bottom-05">{{ field.label }}</h4>
|
||||||
{% if label_description %}
|
{% if label_description %}
|
||||||
<p class="usa-hint margin-top-0 margin-bottom-05">{{ label_description }}</p>
|
<p class="usa-hint margin-top-0 margin-bottom-05">{{ label_description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -11,4 +11,4 @@ Template include for read-only form fields
|
||||||
This allows us to customize the displayed value.
|
This allows us to customize the displayed value.
|
||||||
For instance, Select fields will display the id by default.
|
For instance, Select fields will display the id by default.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<p class="read-only-value">{{ value|default:field.value }}</p>
|
<p class="margin-top-0">{{ value|default:field.value }}</p>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<h4 class="margin-bottom-0 text-primary">Assigned domains</h4>
|
<h4 class="margin-bottom-0">Assigned domains</h4>
|
||||||
{% if domain_count > 0 %}
|
{% if domain_count > 0 %}
|
||||||
<p class="margin-top-0">{{domain_count}}</p>
|
<p class="margin-top-0">{{domain_count}}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<h4 class="margin-bottom-0 text-primary">Member access</h4>
|
<h4 class="margin-bottom-0">Member access</h4>
|
||||||
{% if permissions.roles and 'organization_admin' in permissions.roles %}
|
{% if permissions.roles and 'organization_admin' in permissions.roles %}
|
||||||
<p class="margin-top-0">Admin access</p>
|
<p class="margin-top-0">Admin access</p>
|
||||||
{% elif permissions.roles and 'organization_member' in permissions.roles %}
|
{% elif permissions.roles and 'organization_member' in permissions.roles %}
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
<p class="margin-top-0">⎯</p>
|
<p class="margin-top-0">⎯</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h4 class="margin-bottom-0 text-primary">Organization domain requests</h4>
|
<h4 class="margin-bottom-0">Organization domain requests</h4>
|
||||||
{% if member_has_edit_request_portfolio_permission %}
|
{% if member_has_edit_request_portfolio_permission %}
|
||||||
<p class="margin-top-0">View all requests plus create requests</p>
|
<p class="margin-top-0">View all requests plus create requests</p>
|
||||||
{% elif member_has_view_all_requests_portfolio_permission %}
|
{% elif member_has_view_all_requests_portfolio_permission %}
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<p class="margin-top-0">No access</p>
|
<p class="margin-top-0">No access</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h4 class="margin-bottom-0 text-primary">Organization members</h4>
|
<h4 class="margin-bottom-0">Organization members</h4>
|
||||||
{% if member_has_edit_members_portfolio_permission %}
|
{% if member_has_edit_members_portfolio_permission %}
|
||||||
<p class="margin-top-0">View all members plus manage members</p>
|
<p class="margin-top-0">View all members plus manage members</p>
|
||||||
{% elif member_has_view_members_portfolio_permission %}
|
{% elif member_has_view_members_portfolio_permission %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<h2 class="usa-modal__heading">
|
<h2 class="usa-modal__heading">
|
||||||
{{ modal_heading }}
|
{{ modal_heading }}
|
||||||
{%if domain_name_modal is not None %}
|
{%if domain_name_modal is not None %}
|
||||||
<span class="domain-name-wrap">
|
<span class="string-wrap">
|
||||||
{{ domain_name_modal }}
|
{{ domain_name_modal }}
|
||||||
</span>
|
</span>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if domain_request.alternative_domains.all %}
|
{% if domain_request.alternative_domains.all %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
|
<h4>Alternative domains</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for site in domain_request.alternative_domains.all %}
|
{% for site in domain_request.alternative_domains.all %}
|
||||||
<li>{{ site.website }}</li>
|
<li>{{ site.website }}</li>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
Your contact information
|
Your contact information
|
||||||
</h3>
|
</h3>
|
||||||
<div class="usa-summary-box__text">
|
<div class="usa-summary-box__text">
|
||||||
<ul>
|
<ul class="usa-list">
|
||||||
<li>Full name: <b>{{ user.get_formatted_name }}</b></li>
|
<li>Full name: <b>{{ user.get_formatted_name }}</b></li>
|
||||||
<li>Organization email: <b>{{ user.email }}</b></li>
|
<li>Organization email: <b>{{ user.email }}</b></li>
|
||||||
<li>Title or role in your organization: <b>{{ user.title }}</b></li>
|
<li>Title or role in your organization: <b>{{ user.title }}</b></li>
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if domain_request.alternative_domains.all %}
|
{% if domain_request.alternative_domains.all %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
|
<h4>Alternative domains</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for site in domain_request.alternative_domains.all %}
|
{% for site in domain_request.alternative_domains.all %}
|
||||||
<li>{{ site.website }}</li>
|
<li>{{ site.website }}</li>
|
||||||
|
@ -132,8 +132,8 @@
|
||||||
{% with title=form_titles|get_item:step %}
|
{% with title=form_titles|get_item:step %}
|
||||||
{% if domain_request.has_additional_details %}
|
{% if domain_request.has_additional_details %}
|
||||||
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
|
<h4 class="margin-bottom-0">CISA Regional Representative</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-05">
|
||||||
{% if domain_request.cisa_representative_first_name %}
|
{% if domain_request.cisa_representative_first_name %}
|
||||||
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
||||||
{% if domain_request.cisa_representative_email %}
|
{% if domain_request.cisa_representative_email %}
|
||||||
|
@ -144,8 +144,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
|
<h4 class="margin-bottom-0">Anything else</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-05">
|
||||||
{% if domain_request.anything_else %}
|
{% if domain_request.anything_else %}
|
||||||
{{domain_request.anything_else}}
|
{{domain_request.anything_else}}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -39,34 +39,32 @@
|
||||||
|
|
||||||
{% block status_summary %}
|
{% block status_summary %}
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
class="usa-summary-box margin-top-3 padding-y-2 margin-bottom-1"
|
||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="summary-box-key-information"
|
aria-labelledby="summary-box-key-information"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
<div class="usa-summary-box__text padding-top-0"
|
||||||
id="summary-box-key-information"
|
>
|
||||||
>
|
<p class="font-sans-md margin-y-0 text-primary-darker">
|
||||||
<span class="text-bold text-primary-darker">
|
<strong>Status:</strong>
|
||||||
Status:
|
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
||||||
</span>
|
</p>
|
||||||
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
{% endblock status_summary %}
|
{% endblock status_summary %}
|
||||||
|
|
||||||
{% block status_metadata %}
|
{% block status_metadata %}
|
||||||
|
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
{% if DomainRequest.creator %}
|
{% if DomainRequest.creator %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<p>
|
||||||
<b class="review__step__name">Created by:</b> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
|
<strong class="text-primary-dark">Created by:</strong> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<p>
|
||||||
<b class="review__step__name">No creator found:</b> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
<strong class="text-primary-dark">No creator found:</strong> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -77,49 +75,32 @@
|
||||||
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
||||||
Leave it this way until we've solidified our requirements.
|
Leave it this way until we've solidified our requirements.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% if DomainRequest.status == statuses.STARTED %}
|
|
||||||
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
<p>
|
||||||
<p class="margin-top-1">
|
{% if DomainRequest.status == statuses.STARTED %}
|
||||||
|
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
A newly created domain request will not have a value for last_status update.
|
A newly created domain request will not have a value for last_status update.
|
||||||
This is because the status never really updated.
|
This is because the status never really updated.
|
||||||
However, if this somehow goes back to started we can default to displaying that new date.
|
However, if this somehow goes back to started we can default to displaying that new date.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
|
<strong class="text-primary-dark">Started on:</strong> {{last_status_update|default:first_started_date}}
|
||||||
</p>
|
{% endwith %}
|
||||||
{% endwith %}
|
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
||||||
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<strong class="text-primary-dark">Last updated on:</strong> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
||||||
</p>
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
<p class="margin-top-1">
|
<strong class="text-primary-dark">Last updated on:</strong> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
{% elif DomainRequest.status == statuses.REJECTED %}
|
||||||
</p>
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
<strong class="text-primary-dark">Rejected on:</strong> {{last_status_update}}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
</p>
|
<strong class="text-primary-dark">Withdrawn on:</strong> {{last_status_update}}
|
||||||
<p class="margin-top-1">
|
{% else %}
|
||||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
||||||
</p>
|
<strong class="text-primary-dark">Last updated on:</strong> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
{% elif DomainRequest.status == statuses.REJECTED %}
|
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
|
||||||
</p>
|
|
||||||
<p class="margin-top-1">
|
|
||||||
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
|
|
||||||
</p>
|
|
||||||
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
|
||||||
</p>
|
|
||||||
<p class="margin-top-1">
|
|
||||||
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
|
||||||
<p class="margin-top-1">
|
|
||||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -127,7 +108,7 @@
|
||||||
|
|
||||||
{% block status_blurb %}
|
{% block status_blurb %}
|
||||||
{% if DomainRequest.is_awaiting_review %}
|
{% if DomainRequest.is_awaiting_review %}
|
||||||
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}</p>
|
{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock status_blurb %}
|
{% endblock status_blurb %}
|
||||||
|
|
||||||
|
@ -142,20 +123,19 @@
|
||||||
|
|
||||||
<div class="grid-col maxw-fit-content desktop:grid-offset-2 ">
|
<div class="grid-col maxw-fit-content desktop:grid-offset-2 ">
|
||||||
{% block request_summary_header %}
|
{% block request_summary_header %}
|
||||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
<h2> Summary of your domain request </h2>
|
||||||
{% endblock request_summary_header%}
|
{% endblock request_summary_header%}
|
||||||
|
|
||||||
{% block request_summary %}
|
{% block request_summary %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
|
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with heading_level='h3' %}
|
|
||||||
{% with org_type=DomainRequest.get_generic_org_type_display %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Type of organization' value=org_type %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if DomainRequest.tribe_name %}
|
{% if DomainRequest.tribe_name %}
|
||||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name %}
|
||||||
|
|
||||||
{% if DomainRequest.federally_recognized_tribe %}
|
{% if DomainRequest.federally_recognized_tribe %}
|
||||||
<p>Federally-recognized tribe</p>
|
<p>Federally-recognized tribe</p>
|
||||||
|
@ -168,56 +148,56 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.get_federal_type_display %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.is_election_board %}
|
{% if DomainRequest.is_election_board %}
|
||||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Election office' value=value %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.organization_name %}
|
{% if DomainRequest.organization_name %}
|
||||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.about_your_organization %}
|
{% if DomainRequest.about_your_organization %}
|
||||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.senior_official %}
|
{% if DomainRequest.senior_official %}
|
||||||
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.current_websites.all %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.requested_domain %}
|
{% if DomainRequest.requested_domain %}
|
||||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.alternative_domains.all %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.purpose %}
|
{% if DomainRequest.purpose %}
|
||||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.creator %}
|
{% if DomainRequest.creator %}
|
||||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.other_contacts.all %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# We always show this field even if None #}
|
{# We always show this field even if None #}
|
||||||
{% if DomainRequest %}
|
{% if DomainRequest %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
|
<h4 class="margin-bottom-0">CISA Regional Representative</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.cisa_representative_first_name %}
|
{% if DomainRequest.cisa_representative_first_name %}
|
||||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||||
|
@ -225,7 +205,7 @@
|
||||||
No
|
No
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
|
<h4 class="margin-bottom-0">Anything else</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.anything_else %}
|
{% if DomainRequest.anything_else %}
|
||||||
{{DomainRequest.anything_else}}
|
{{DomainRequest.anything_else}}
|
||||||
|
@ -234,7 +214,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock request_summary%}
|
{% endblock request_summary%}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,10 +9,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3
|
<h3
|
||||||
{% endif %}
|
{% endif %}
|
||||||
class="summary-item__title
|
class="margin-top-0 margin-bottom-05
|
||||||
font-sans-md
|
|
||||||
text-primary-dark text-semibold
|
|
||||||
margin-top-0 margin-bottom-05
|
|
||||||
padding-right-1"
|
padding-right-1"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
@ -22,7 +19,7 @@
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub_header_text %}
|
{% if sub_header_text %}
|
||||||
<h4 class="header--body text-primary-dark margin-bottom-0">{{ sub_header_text }}</h4>
|
<h4 class="margin-bottom-0">{{ sub_header_text }}</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if permissions %}
|
{% if permissions %}
|
||||||
{% include "includes/member_permissions.html" with permissions=value %}
|
{% include "includes/member_permissions.html" with permissions=value %}
|
||||||
|
@ -40,9 +37,7 @@
|
||||||
{% for item in value %}
|
{% for item in value %}
|
||||||
<dt>
|
<dt>
|
||||||
|
|
||||||
<h4 class="summary-item__title
|
<h4 class="
|
||||||
font-sans-md
|
|
||||||
text-primary-dark text-semibold
|
|
||||||
margin-bottom-05
|
margin-bottom-05
|
||||||
padding-right-1">
|
padding-right-1">
|
||||||
Contact {{forloop.counter}}
|
Contact {{forloop.counter}}
|
||||||
|
@ -119,7 +114,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if value.invitations.all %}
|
{% if value.invitations.all %}
|
||||||
<h4 class="h4--sm-05">Invited domain managers</h4>
|
<h4 class="margin-bottom-05">Invited domain managers</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for item in value.invitations.all %}
|
{% for item in value.invitations.all %}
|
||||||
<li>{{ item.email }}</li>
|
<li>{{ item.email }}</li>
|
||||||
|
@ -143,7 +138,7 @@
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a
|
<a
|
||||||
href="{{ edit_link }}"
|
href="{{ edit_link }}"
|
||||||
class="usa-link usa-link--icon font-sans-sm line-height-sans-5"
|
class="usa-link usa-link--icon font-sans-sm line-height-sans-4"
|
||||||
>
|
>
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#{% if manage_button %}settings{% elif view_button %}visibility{% else %}edit{% endif %}"></use>
|
<use xlink:href="{% static 'img/sprite.svg' %}#{% if manage_button %}settings{% elif view_button %}visibility{% else %}edit{% endif %}"></use>
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
<section id="domain-assignments-readonly-view" class="display-none">
|
<section id="domain-assignments-readonly-view" class="display-none">
|
||||||
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
||||||
|
|
||||||
<h2 class="text-primary-dark">Would you like to continue with the following domain assignment changes for
|
<h2>Would you like to continue with the following domain assignment changes for
|
||||||
{% if member %}
|
{% if member %}
|
||||||
{{ member.email }}
|
{{ member.email }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -88,13 +88,13 @@
|
||||||
|
|
||||||
<div id="domain-assignments-summary" class="margin-bottom-2">
|
<div id="domain-assignments-summary" class="margin-bottom-2">
|
||||||
<!-- AJAX will populate this summary -->
|
<!-- AJAX will populate this summary -->
|
||||||
<h3 class="header--body text-primary margin-bottom-1">Unassigned domains</h3>
|
<h3 class="margin-bottom-1">Unassigned domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled">
|
<ul class="usa-list usa-list--unstyled">
|
||||||
<li>item1</li>
|
<li>item1</li>
|
||||||
<li>item2</li>
|
<li>item2</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Assigned domains</h3>
|
<h3 class="margin-bottom-0">Assigned domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled">
|
<ul class="usa-list usa-list--unstyled">
|
||||||
<li>item1</li>
|
<li>item1</li>
|
||||||
<li>item2</li>
|
<li>item2</li>
|
||||||
|
|
|
@ -95,17 +95,15 @@
|
||||||
<h2>Admin access permissions</h2>
|
<h2>Admin access permissions</h2>
|
||||||
<p>Member permissions available for admin-level acccess.</p>
|
<p>Member permissions available for admin-level acccess.</p>
|
||||||
|
|
||||||
<h3 class="summary-item__title
|
<h3 class="
|
||||||
text-primary-dark
|
|
||||||
margin-bottom-0">Organization domain requests</h3>
|
margin-bottom-0">Organization domain requests</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
||||||
{% input_with_errors form.domain_request_permission_admin %}
|
{% input_with_errors form.domain_request_permission_admin %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<h3 class="summary-item__title
|
<h3 class="
|
||||||
text-primary-dark
|
|
||||||
margin-bottom-0
|
margin-bottom-0
|
||||||
margin-top-3">Organization members</h3>
|
margin-top-4">Organization members</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
||||||
{% input_with_errors form.member_permission_admin %}
|
{% input_with_errors form.member_permission_admin %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -116,7 +114,7 @@
|
||||||
<h2>Basic member permissions</h2>
|
<h2>Basic member permissions</h2>
|
||||||
<p>Member permissions available for basic-level acccess.</p>
|
<p>Member permissions available for basic-level acccess.</p>
|
||||||
|
|
||||||
<h3 class="margin-bottom-0 summary-item__title text-primary-dark">Organization domain requests</h3>
|
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
||||||
{% input_with_errors form.domain_request_permission_member %}
|
{% input_with_errors form.domain_request_permission_member %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -68,17 +68,15 @@
|
||||||
<h2>Admin access permissions</h2>
|
<h2>Admin access permissions</h2>
|
||||||
<p>Member permissions available for admin-level acccess.</p>
|
<p>Member permissions available for admin-level acccess.</p>
|
||||||
|
|
||||||
<h3 class="summary-item__title
|
<h3 class="
|
||||||
text-primary-dark
|
|
||||||
margin-bottom-0">Organization domain requests</h3>
|
margin-bottom-0">Organization domain requests</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
||||||
{% input_with_errors form.domain_request_permission_admin %}
|
{% input_with_errors form.domain_request_permission_admin %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<h3 class="summary-item__title
|
<h3 class="
|
||||||
text-primary-dark
|
|
||||||
margin-bottom-0
|
margin-bottom-0
|
||||||
margin-top-3">Organization members</h3>
|
margin-top-4">Organization members</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
|
||||||
{% input_with_errors form.member_permission_admin %}
|
{% input_with_errors form.member_permission_admin %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -127,37 +125,34 @@
|
||||||
<h2 class="usa-modal__heading" id="invite-member-heading">
|
<h2 class="usa-modal__heading" id="invite-member-heading">
|
||||||
Invite this member to the organization?
|
Invite this member to the organization?
|
||||||
</h2>
|
</h2>
|
||||||
<h3 class="summary-item__title
|
<h3>Member information and permissions</h3>
|
||||||
text-primary-dark">Member information and permissions</h3>
|
<!-- Display email as a header and access level -->
|
||||||
<div class="usa-prose">
|
<h4 class="margin-bottom-0">Email</h4>
|
||||||
<!-- Display email as a header and access level -->
|
<p class="margin-top-0" id="modalEmail"></p>
|
||||||
<h4 class="text-primary">Email</h4>
|
|
||||||
<p class="margin-top-0" id="modalEmail"></p>
|
|
||||||
|
|
||||||
<h4 class="text-primary">Member Access</h4>
|
<h4 class="margin-bottom-0">Member Access</h4>
|
||||||
<p class="margin-top-0" id="modalAccessLevel"></p>
|
<p class="margin-top-0" id="modalAccessLevel"></p>
|
||||||
|
|
||||||
<!-- Dynamic Permissions Details -->
|
<!-- Dynamic Permissions Details -->
|
||||||
<div id="permission_details"></div>
|
<div id="permission_details"></div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="usa-modal__footer">
|
<div class="usa-modal__footer">
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group">
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button id="confirm_new_member_submit" type="submit" class="usa-button">Yes, invite member</button>
|
<button id="confirm_new_member_submit" type="submit" class="usa-button">Yes, invite member</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-button--unstyled"
|
class="usa-button usa-button--unstyled"
|
||||||
data-close-modal
|
data-close-modal
|
||||||
onclick="closeModal()"
|
onclick="closeModal()"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate>
|
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h4 class="read-only-label">Organization name</h4>
|
<h4 class="margin-bottom-05">Organization name</h4>
|
||||||
<p class="read-only-value">
|
<p class="margin-top-0">
|
||||||
{{ portfolio.federal_agency }}
|
{{ portfolio.federal_agency }}
|
||||||
</p>
|
</p>
|
||||||
{% input_with_errors form.address_line1 %}
|
{% input_with_errors form.address_line1 %}
|
||||||
|
@ -53,8 +53,8 @@
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h4 class="read-only-label">Organization name</h4>
|
<h4 class="margin-bottom-05">Organization name</h4>
|
||||||
<p class="read-only-value">
|
<p class="margin-top-0">
|
||||||
{{ portfolio.federal_agency }}
|
{{ portfolio.federal_agency }}
|
||||||
</p>
|
</p>
|
||||||
{% if form.address_line1.value is not None %}
|
{% if form.address_line1.value is not None %}
|
||||||
|
|
|
@ -12,7 +12,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||||
@patch("registrar.utility.email_invitations.validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations.get_requestor_email")
|
@patch("registrar.utility.email_invitations.get_requestor_email")
|
||||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||||
@patch("registrar.utility.email_invitations.normalize_domains")
|
@patch("registrar.utility.email_invitations.normalize_domains")
|
||||||
|
@ -57,7 +57,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_normalize_domains.assert_called_once_with(mock_domain)
|
mock_normalize_domains.assert_called_once_with(mock_domain)
|
||||||
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
|
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
|
||||||
mock_validate_invitation.assert_called_once_with(
|
mock_validate_invitation.assert_called_once_with(
|
||||||
email, [mock_domain], mock_requestor, is_member_of_different_org
|
email, None, [mock_domain], mock_requestor, is_member_of_different_org
|
||||||
)
|
)
|
||||||
mock_send_invitation_email.assert_called_once_with(email, mock_requestor_email, [mock_domain], None)
|
mock_send_invitation_email.assert_called_once_with(email, mock_requestor_email, [mock_domain], None)
|
||||||
mock_user_domain_role_filter.assert_called_once_with(domain=mock_domain)
|
mock_user_domain_role_filter.assert_called_once_with(domain=mock_domain)
|
||||||
|
@ -77,7 +77,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@patch("registrar.utility.email_invitations.send_templated_email")
|
@patch("registrar.utility.email_invitations.send_templated_email")
|
||||||
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
|
||||||
@patch("registrar.utility.email_invitations.validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations.get_requestor_email")
|
@patch("registrar.utility.email_invitations.get_requestor_email")
|
||||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||||
@patch("registrar.utility.email_invitations.normalize_domains")
|
@patch("registrar.utility.email_invitations.normalize_domains")
|
||||||
|
@ -136,7 +136,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_normalize_domains.assert_called_once_with([mock_domain1, mock_domain2])
|
mock_normalize_domains.assert_called_once_with([mock_domain1, mock_domain2])
|
||||||
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain1, mock_domain2])
|
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain1, mock_domain2])
|
||||||
mock_validate_invitation.assert_called_once_with(
|
mock_validate_invitation.assert_called_once_with(
|
||||||
email, [mock_domain1, mock_domain2], mock_requestor, is_member_of_different_org
|
email, None, [mock_domain1, mock_domain2], mock_requestor, is_member_of_different_org
|
||||||
)
|
)
|
||||||
mock_send_invitation_email.assert_called_once_with(
|
mock_send_invitation_email.assert_called_once_with(
|
||||||
email, mock_requestor_email, [mock_domain1, mock_domain2], None
|
email, mock_requestor_email, [mock_domain1, mock_domain2], None
|
||||||
|
@ -175,7 +175,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
self.assertEqual(mock_send_templated_email.call_count, 2)
|
self.assertEqual(mock_send_templated_email.call_count, 2)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@patch("registrar.utility.email_invitations.validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
def test_send_domain_invitation_email_raises_invite_validation_exception(self, mock_validate_invitation):
|
def test_send_domain_invitation_email_raises_invite_validation_exception(self, mock_validate_invitation):
|
||||||
"""Test sending domain invitation email for one domain and assert exception
|
"""Test sending domain invitation email for one domain and assert exception
|
||||||
when invite validation fails.
|
when invite validation fails.
|
||||||
|
@ -213,7 +213,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_get_requestor_email.assert_called_once()
|
mock_get_requestor_email.assert_called_once()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@patch("registrar.utility.email_invitations.validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations.get_requestor_email")
|
@patch("registrar.utility.email_invitations.get_requestor_email")
|
||||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||||
@patch("registrar.utility.email_invitations.normalize_domains")
|
@patch("registrar.utility.email_invitations.normalize_domains")
|
||||||
|
@ -257,13 +257,13 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_normalize_domains.assert_called_once_with(mock_domain)
|
mock_normalize_domains.assert_called_once_with(mock_domain)
|
||||||
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
|
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
|
||||||
mock_validate_invitation.assert_called_once_with(
|
mock_validate_invitation.assert_called_once_with(
|
||||||
email, [mock_domain], mock_requestor, is_member_of_different_org
|
email, None, [mock_domain], mock_requestor, is_member_of_different_org
|
||||||
)
|
)
|
||||||
self.assertEqual(str(context.exception), "Error sending email")
|
self.assertEqual(str(context.exception), "Error sending email")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
@patch("registrar.utility.email_invitations.send_emails_to_domain_managers")
|
@patch("registrar.utility.email_invitations.send_emails_to_domain_managers")
|
||||||
@patch("registrar.utility.email_invitations.validate_invitation")
|
@patch("registrar.utility.email_invitations._validate_invitation")
|
||||||
@patch("registrar.utility.email_invitations.get_requestor_email")
|
@patch("registrar.utility.email_invitations.get_requestor_email")
|
||||||
@patch("registrar.utility.email_invitations.send_invitation_email")
|
@patch("registrar.utility.email_invitations.send_invitation_email")
|
||||||
@patch("registrar.utility.email_invitations.normalize_domains")
|
@patch("registrar.utility.email_invitations.normalize_domains")
|
||||||
|
@ -305,7 +305,7 @@ class DomainInvitationEmail(unittest.TestCase):
|
||||||
mock_normalize_domains.assert_called_once_with(mock_domain)
|
mock_normalize_domains.assert_called_once_with(mock_domain)
|
||||||
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
|
mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
|
||||||
mock_validate_invitation.assert_called_once_with(
|
mock_validate_invitation.assert_called_once_with(
|
||||||
email, [mock_domain], mock_requestor, is_member_of_different_org
|
email, None, [mock_domain], mock_requestor, is_member_of_different_org
|
||||||
)
|
)
|
||||||
mock_send_invitation_email.assert_called_once_with(email, mock_requestor_email, [mock_domain], None)
|
mock_send_invitation_email.assert_called_once_with(email, mock_requestor_email, [mock_domain], None)
|
||||||
self.assertEqual(str(context.exception), "Error sending email")
|
self.assertEqual(str(context.exception), "Error sending email")
|
||||||
|
|
|
@ -349,6 +349,70 @@ class TestDomainCache(MockEppLib):
|
||||||
class TestDomainCreation(MockEppLib):
|
class TestDomainCreation(MockEppLib):
|
||||||
"""Rule: An approved domain request must result in a domain"""
|
"""Rule: An approved domain request must result in a domain"""
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_or_create_public_contact_race_condition(self):
|
||||||
|
"""
|
||||||
|
Scenario: Two processes try to create the same security contact simultaneously
|
||||||
|
Given a domain in UNKNOWN state
|
||||||
|
When a race condition occurs during contact creation
|
||||||
|
Then no IntegrityError is raised
|
||||||
|
And only one security contact exists in database
|
||||||
|
And the correct public contact is returned
|
||||||
|
|
||||||
|
CONTEXT: We ran into an intermittent but somewhat rare issue where IntegrityError
|
||||||
|
was raised when creating PublicContact.
|
||||||
|
Per our logs, this seemed to appear during periods of high app activity.
|
||||||
|
"""
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov")
|
||||||
|
|
||||||
|
self.first_call = True
|
||||||
|
|
||||||
|
def mock_filter(*args, **kwargs):
|
||||||
|
"""Simulates a race condition by creating a
|
||||||
|
duplicate contact between the first filter and save.
|
||||||
|
"""
|
||||||
|
# Return an empty queryset for the first call. Otherwise just proceed as normal.
|
||||||
|
if self.first_call:
|
||||||
|
self.first_call = False
|
||||||
|
duplicate = PublicContact(
|
||||||
|
domain=domain,
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.SECURITY,
|
||||||
|
registry_id="defaultSec",
|
||||||
|
email="dotgov@cisa.dhs.gov",
|
||||||
|
name="Registry Customer Service",
|
||||||
|
)
|
||||||
|
duplicate.save(skip_epp_save=True)
|
||||||
|
return PublicContact.objects.none()
|
||||||
|
|
||||||
|
return PublicContact.objects.filter(*args, **kwargs)
|
||||||
|
|
||||||
|
with patch.object(PublicContact.objects, "filter", side_effect=mock_filter):
|
||||||
|
try:
|
||||||
|
public_contact = PublicContact(
|
||||||
|
domain=domain,
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.SECURITY,
|
||||||
|
registry_id="defaultSec",
|
||||||
|
email="dotgov@cisa.dhs.gov",
|
||||||
|
name="Registry Customer Service",
|
||||||
|
)
|
||||||
|
returned_public_contact = domain._get_or_create_public_contact(public_contact)
|
||||||
|
except IntegrityError:
|
||||||
|
self.fail(
|
||||||
|
"IntegrityError was raised during contact creation due to a race condition. "
|
||||||
|
"This indicates that concurrent contact creation is not working in some cases. "
|
||||||
|
"The error occurs when two processes try to create the same contact simultaneously. "
|
||||||
|
"Expected behavior: gracefully handle duplicate creation and return existing contact."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify that only one contact exists and its correctness
|
||||||
|
security_contacts = PublicContact.objects.filter(
|
||||||
|
domain=domain, contact_type=PublicContact.ContactTypeChoices.SECURITY
|
||||||
|
)
|
||||||
|
self.assertEqual(security_contacts.count(), 1)
|
||||||
|
self.assertEqual(returned_public_contact, security_contacts.get())
|
||||||
|
self.assertEqual(returned_public_contact.registry_id, "defaultSec")
|
||||||
|
self.assertEqual(returned_public_contact.email, "dotgov@cisa.dhs.gov")
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_approved_domain_request_creates_domain_locally(self):
|
def test_approved_domain_request_creates_domain_locally(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -54,6 +54,7 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
title="Admin",
|
title="Admin",
|
||||||
)
|
)
|
||||||
self.email6 = "fifth@example.com"
|
self.email6 = "fifth@example.com"
|
||||||
|
self.email7 = "sixth@example.com"
|
||||||
|
|
||||||
# Create Portfolio
|
# Create Portfolio
|
||||||
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
||||||
|
@ -302,7 +303,7 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
@override_flag("organization_members", active=True)
|
@override_flag("organization_members", active=True)
|
||||||
def test_get_portfolio_invited_json_with_domains(self):
|
def test_get_portfolio_invited_json_with_domains(self):
|
||||||
"""Test that portfolio invited members are returned properly for an authenticated user and the response includes
|
"""Test that portfolio invited members are returned properly for an authenticated user and the response includes
|
||||||
the domains that the member manages.."""
|
the domains that the member manages. Test also verifies that retrieved invitations are not included."""
|
||||||
UserPortfolioPermission.objects.create(
|
UserPortfolioPermission.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
portfolio=self.portfolio,
|
portfolio=self.portfolio,
|
||||||
|
@ -319,6 +320,16 @@ class GetPortfolioMembersJsonTest(MockEppLib, WebTest):
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
PortfolioInvitation.objects.create(
|
||||||
|
email=self.email7,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
],
|
||||||
|
status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED,
|
||||||
|
)
|
||||||
|
|
||||||
# create a domain in the portfolio
|
# create a domain in the portfolio
|
||||||
domain = Domain.objects.create(
|
domain = Domain.objects.create(
|
||||||
|
|
|
@ -211,11 +211,11 @@ class TestPortfolio(WebTest):
|
||||||
# Assert the response is a 200
|
# Assert the response is a 200
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# The label for Federal agency will always be a h4
|
# The label for Federal agency will always be a h4
|
||||||
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
|
self.assertContains(response, '<h4 class="margin-bottom-05">Organization name</h4>')
|
||||||
# The read only label for city will be a h4
|
# The read only label for city will be a h4
|
||||||
self.assertContains(response, '<h4 class="read-only-label">City</h4>')
|
self.assertContains(response, '<h4 class="margin-bottom-05">City</h4>')
|
||||||
self.assertNotContains(response, 'for="id_city"')
|
self.assertNotContains(response, 'for="id_city"')
|
||||||
self.assertContains(response, '<p class="read-only-value">Los Angeles</p>')
|
self.assertContains(response, '<p class="margin-top-0">Los Angeles</p>')
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_portfolio_organization_page_edit_access(self):
|
def test_portfolio_organization_page_edit_access(self):
|
||||||
|
@ -236,10 +236,10 @@ class TestPortfolio(WebTest):
|
||||||
# Assert the response is a 200
|
# Assert the response is a 200
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# The label for Federal agency will always be a h4
|
# The label for Federal agency will always be a h4
|
||||||
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
|
self.assertContains(response, '<h4 class="margin-bottom-05">Organization name</h4>')
|
||||||
# The read only label for city will be a h4
|
# The read only label for city will be a h4
|
||||||
self.assertNotContains(response, '<h4 class="read-only-label">City</h4>')
|
self.assertNotContains(response, '<h4 class="margin-bottom-05">City</h4>')
|
||||||
self.assertNotContains(response, '<p class="read-only-value">Los Angeles</p>')
|
self.assertNotContains(response, '<p class="margin-top-0">Los Angeles</p>')
|
||||||
self.assertContains(response, 'for="id_city"')
|
self.assertContains(response, 'for="id_city"')
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -2879,7 +2879,7 @@ class TestRequestingEntity(WebTest):
|
||||||
|
|
||||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-sub_organization"] = ""
|
form["portfolio_requesting_entity-sub_organization"] = "other"
|
||||||
|
|
||||||
form["portfolio_requesting_entity-requested_suborganization"] = "moon"
|
form["portfolio_requesting_entity-requested_suborganization"] = "moon"
|
||||||
form["portfolio_requesting_entity-suborganization_city"] = "kepler"
|
form["portfolio_requesting_entity-suborganization_city"] = "kepler"
|
||||||
|
@ -2942,18 +2942,34 @@ class TestRequestingEntity(WebTest):
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# For 2 the tests below, it is required to submit a form without submitting a value
|
||||||
|
# for the select/combobox. WebTest will not do this; by default, WebTest will submit
|
||||||
|
# the first choice in a select. So, need to manipulate the form to remove the
|
||||||
|
# particular select/combobox that will not be submitted, and then post the form.
|
||||||
|
form_action = f"/request/{domain_request.pk}/portfolio_requesting_entity/"
|
||||||
|
|
||||||
# Test missing suborganization selection
|
# Test missing suborganization selection
|
||||||
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-sub_organization"] = ""
|
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = False
|
||||||
|
# remove sub_organization from the form submission
|
||||||
response = form.submit()
|
form_data = form.submit_fields()
|
||||||
|
form_data = [(key, value) for key, value in form_data if key != "portfolio_requesting_entity-sub_organization"]
|
||||||
|
response = self.app.post(form_action, dict(form_data))
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
self.assertContains(response, "Suborganization is required.", status_code=200)
|
self.assertContains(response, "Suborganization is required.", status_code=200)
|
||||||
|
|
||||||
# Test missing custom suborganization details
|
# Test missing custom suborganization details
|
||||||
|
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = True
|
||||||
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True
|
||||||
response = form.submit()
|
form["portfolio_requesting_entity-sub_organization"] = "other"
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
# remove suborganization_state_territory from the form submission
|
||||||
|
form_data = form.submit_fields()
|
||||||
|
form_data = [
|
||||||
|
(key, value)
|
||||||
|
for key, value in form_data
|
||||||
|
if key != "portfolio_requesting_entity-suborganization_state_territory"
|
||||||
|
]
|
||||||
|
response = self.app.post(form_action, dict(form_data))
|
||||||
self.assertContains(response, "Enter the name of your suborganization.", status_code=200)
|
self.assertContains(response, "Enter the name of your suborganization.", status_code=200)
|
||||||
self.assertContains(response, "Enter the city where your suborganization is located.", status_code=200)
|
self.assertContains(response, "Enter the city where your suborganization is located.", status_code=200)
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
|
|
|
@ -37,7 +37,7 @@ def send_domain_invitation_email(
|
||||||
domains = normalize_domains(domains)
|
domains = normalize_domains(domains)
|
||||||
requestor_email = get_requestor_email(requestor, domains)
|
requestor_email = get_requestor_email(requestor, domains)
|
||||||
|
|
||||||
validate_invitation(email, domains, requestor, is_member_of_different_org)
|
_validate_invitation(email, requested_user, domains, requestor, is_member_of_different_org)
|
||||||
|
|
||||||
send_invitation_email(email, requestor_email, domains, requested_user)
|
send_invitation_email(email, requestor_email, domains, requested_user)
|
||||||
|
|
||||||
|
@ -101,12 +101,12 @@ def get_requestor_email(requestor, domains):
|
||||||
return requestor.email
|
return requestor.email
|
||||||
|
|
||||||
|
|
||||||
def validate_invitation(email, domains, requestor, is_member_of_different_org):
|
def _validate_invitation(email, user, domains, requestor, is_member_of_different_org):
|
||||||
"""Validate the invitation conditions."""
|
"""Validate the invitation conditions."""
|
||||||
check_outside_org_membership(email, requestor, is_member_of_different_org)
|
check_outside_org_membership(email, requestor, is_member_of_different_org)
|
||||||
|
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
validate_existing_invitation(email, domain)
|
_validate_existing_invitation(email, user, domain)
|
||||||
|
|
||||||
# NOTE: should we also be validating against existing user_domain_roles
|
# NOTE: should we also be validating against existing user_domain_roles
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ def check_outside_org_membership(email, requestor, is_member_of_different_org):
|
||||||
raise OutsideOrgMemberError(email=email)
|
raise OutsideOrgMemberError(email=email)
|
||||||
|
|
||||||
|
|
||||||
def validate_existing_invitation(email, domain):
|
def _validate_existing_invitation(email, user, domain):
|
||||||
"""Check for existing invitations and handle their status."""
|
"""Check for existing invitations and handle their status."""
|
||||||
try:
|
try:
|
||||||
invite = DomainInvitation.objects.get(email=email, domain=domain)
|
invite = DomainInvitation.objects.get(email=email, domain=domain)
|
||||||
|
@ -134,6 +134,9 @@ def validate_existing_invitation(email, domain):
|
||||||
raise AlreadyDomainInvitedError(email)
|
raise AlreadyDomainInvitedError(email)
|
||||||
except DomainInvitation.DoesNotExist:
|
except DomainInvitation.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
if user:
|
||||||
|
if UserDomainRole.objects.filter(user=user, domain=domain).exists():
|
||||||
|
raise AlreadyDomainManagerError(email)
|
||||||
|
|
||||||
|
|
||||||
def send_invitation_email(email, requestor_email, domains, requested_user):
|
def send_invitation_email(email, requestor_email, domains, requested_user):
|
||||||
|
|
|
@ -59,13 +59,18 @@ class MissingEmailError(InvitationError):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
class OutsideOrgMemberError(ValueError):
|
class OutsideOrgMemberError(InvitationError):
|
||||||
"""
|
"""
|
||||||
Error raised when an org member tries adding a user from a different .gov org.
|
Error raised when an org member tries adding a user from a different .gov org.
|
||||||
To be deleted when users can be members of multiple orgs.
|
To be deleted when users can be members of multiple orgs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
def __init__(self, email=None):
|
||||||
|
# Default message if no additional info is provided
|
||||||
|
message = "Can not invite member of a .gov organization to a different organization."
|
||||||
|
if email:
|
||||||
|
message = f"{email} is already a member of another .gov organization."
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
class ActionNotAllowed(Exception):
|
class ActionNotAllowed(Exception):
|
||||||
|
|
|
@ -137,7 +137,9 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
)
|
)
|
||||||
|
|
||||||
# PortfolioInvitation query
|
# PortfolioInvitation query
|
||||||
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
|
invitations = PortfolioInvitation.objects.filter(
|
||||||
|
portfolio=portfolio, status=PortfolioInvitation.PortfolioInvitationStatus.INVITED
|
||||||
|
)
|
||||||
invitations = invitations.annotate(
|
invitations = invitations.annotate(
|
||||||
first_name=Value(None, output_field=CharField()),
|
first_name=Value(None, output_field=CharField()),
|
||||||
last_name=Value(None, output_field=CharField()),
|
last_name=Value(None, output_field=CharField()),
|
||||||
|
|
|
@ -67,14 +67,8 @@ def handle_invitation_exceptions(request, exception, email):
|
||||||
messages.error(request, str(exception))
|
messages.error(request, str(exception))
|
||||||
logger.error(str(exception), exc_info=True)
|
logger.error(str(exception), exc_info=True)
|
||||||
elif isinstance(exception, OutsideOrgMemberError):
|
elif isinstance(exception, OutsideOrgMemberError):
|
||||||
logger.warning(
|
messages.error(request, str(exception))
|
||||||
"Could not send email. Can not invite member of a .gov organization to a different organization.",
|
logger.warning(str(exception), exc_info=True)
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
messages.error(
|
|
||||||
request,
|
|
||||||
f"{email} is already a member of another .gov organization.",
|
|
||||||
)
|
|
||||||
elif isinstance(exception, AlreadyDomainManagerError):
|
elif isinstance(exception, AlreadyDomainManagerError):
|
||||||
messages.warning(request, str(exception))
|
messages.warning(request, str(exception))
|
||||||
elif isinstance(exception, AlreadyDomainInvitedError):
|
elif isinstance(exception, AlreadyDomainInvitedError):
|
||||||
|
|
|
@ -65,7 +65,6 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||||
|
|
||||||
def is_editable(self):
|
def is_editable(self):
|
||||||
"""Returns whether domain is editable in the context of the view"""
|
"""Returns whether domain is editable in the context of the view"""
|
||||||
logger.info("checking if is_editable")
|
|
||||||
domain_editable = self.object.is_editable()
|
domain_editable = self.object.is_editable()
|
||||||
if not domain_editable:
|
if not domain_editable:
|
||||||
return False
|
return False
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue