mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-04 08:52:16 +02:00
Merge branch 'main' into dk/3532-epplibwrapper-error
This commit is contained in:
commit
6927d9c5b7
53 changed files with 2446 additions and 822 deletions
|
@ -4,7 +4,7 @@ verify_ssl = true
|
|||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
django = "4.2.17"
|
||||
django = "4.2.20"
|
||||
cfenv = "*"
|
||||
django-cors-headers = "*"
|
||||
pycryptodomex = "*"
|
||||
|
@ -34,6 +34,7 @@ tblib = "*"
|
|||
django-admin-multiple-choice-list-filter = "*"
|
||||
django-import-export = "*"
|
||||
django-waffle = "*"
|
||||
cryptography = "*"
|
||||
|
||||
[dev-packages]
|
||||
django-debug-toolbar = "*"
|
||||
|
|
797
src/Pipfile.lock
generated
797
src/Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1261,6 +1261,13 @@ class HostIpAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
|||
resource_classes = [HostIpResource]
|
||||
model = models.HostIP
|
||||
|
||||
search_fields = ["host__name", "address"]
|
||||
search_help_text = "Search by host name or address."
|
||||
list_display = (
|
||||
"host",
|
||||
"address",
|
||||
)
|
||||
|
||||
|
||||
class ContactResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -4535,6 +4542,10 @@ class PublicContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
|||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
autocomplete_fields = ["domain"]
|
||||
list_display = ("registry_id", "contact_type", "domain", "name")
|
||||
search_fields = ["registry_id", "domain__name", "name"]
|
||||
search_help_text = "Search by registry id, domain, or name."
|
||||
list_filter = ("contact_type",)
|
||||
|
||||
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||
if extra_context is None:
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainDSData() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let domain_dsdata_page = document.getElementById("domain-dsdata");
|
||||
if (domain_dsdata_page) {
|
||||
const override_button = document.getElementById("disable-override-click-button");
|
||||
const cancel_button = document.getElementById("btn-cancel-click-button");
|
||||
const cancel_close_button = document.getElementById("btn-cancel-click-close-button");
|
||||
if (override_button) {
|
||||
override_button.addEventListener("click", function () {
|
||||
submitForm("disable-override-click-form");
|
||||
});
|
||||
}
|
||||
if (cancel_button) {
|
||||
cancel_button.addEventListener("click", function () {
|
||||
submitForm("btn-cancel-click-form");
|
||||
});
|
||||
}
|
||||
if (cancel_close_button) {
|
||||
cancel_close_button.addEventListener("click", function () {
|
||||
submitForm("btn-cancel-click-form");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
521
src/registrar/assets/src/js/getgov/form-dsdata.js
Normal file
521
src/registrar/assets/src/js/getgov/form-dsdata.js
Normal file
|
@ -0,0 +1,521 @@
|
|||
import { showElement, hideElement, scrollToElement } from './helpers';
|
||||
import { removeErrorsFromElement, removeFormErrors } from './form-helpers';
|
||||
|
||||
export class DSDataForm {
|
||||
constructor() {
|
||||
this.addDSDataButton = document.getElementById('dsdata-add-button');
|
||||
this.addDSDataForm = document.querySelector('.add-dsdata-form');
|
||||
this.formChanged = false;
|
||||
this.callback = null;
|
||||
|
||||
// Bind event handlers to maintain 'this' context
|
||||
this.handleAddFormClick = this.handleAddFormClick.bind(this);
|
||||
this.handleEditClick = this.handleEditClick.bind(this);
|
||||
this.handleDeleteClick = this.handleDeleteClick.bind(this);
|
||||
this.handleDeleteKebabClick = this.handleDeleteKebabClick.bind(this);
|
||||
this.handleCancelClick = this.handleCancelClick.bind(this);
|
||||
this.handleCancelAddFormClick = this.handleCancelAddFormClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the DSDataForm by setting up display and event listeners.
|
||||
*/
|
||||
init() {
|
||||
this.initializeDSDataFormDisplay();
|
||||
this.initializeEventListeners();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines the initial display state of the DS data form,
|
||||
* handling validation errors and setting visibility of elements accordingly.
|
||||
*/
|
||||
initializeDSDataFormDisplay() {
|
||||
|
||||
// This check indicates that there is an Add DS Data form
|
||||
// and that form has errors in it. In this case, show the form, and indicate that the form has
|
||||
// changed.
|
||||
if (this.addDSDataForm && this.addDSDataForm.querySelector('.usa-input--error')) {
|
||||
showElement(this.addDSDataForm);
|
||||
this.formChanged = true;
|
||||
}
|
||||
|
||||
// handle display of table view errors
|
||||
// if error exists in an edit-row, make that row show, and readonly row hide
|
||||
const formTable = document.getElementById('dsdata-table')
|
||||
if (formTable) {
|
||||
const editRows = formTable.querySelectorAll('.edit-row');
|
||||
editRows.forEach(editRow => {
|
||||
if (editRow.querySelector('.usa-input--error')) {
|
||||
const readOnlyRow = editRow.previousElementSibling;
|
||||
this.formChanged = true;
|
||||
showElement(editRow);
|
||||
hideElement(readOnlyRow);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches event listeners to relevant UI elements for interaction handling.
|
||||
*/
|
||||
initializeEventListeners() {
|
||||
this.addDSDataButton.addEventListener('click', this.handleAddFormClick);
|
||||
|
||||
const editButtons = document.querySelectorAll('.dsdata-edit');
|
||||
editButtons.forEach(editButton => {
|
||||
editButton.addEventListener('click', this.handleEditClick);
|
||||
});
|
||||
|
||||
const cancelButtons = document.querySelectorAll('.dsdata-cancel');
|
||||
cancelButtons.forEach(cancelButton => {
|
||||
cancelButton.addEventListener('click', this.handleCancelClick);
|
||||
});
|
||||
|
||||
const cancelAddFormButtons = document.querySelectorAll('.dsdata-cancel-add-form');
|
||||
cancelAddFormButtons.forEach(cancelAddFormButton => {
|
||||
cancelAddFormButton.addEventListener('click', this.handleCancelAddFormClick);
|
||||
});
|
||||
|
||||
const deleteButtons = document.querySelectorAll('.dsdata-delete');
|
||||
deleteButtons.forEach(deleteButton => {
|
||||
deleteButton.addEventListener('click', this.handleDeleteClick);
|
||||
});
|
||||
|
||||
const deleteKebabButtons = document.querySelectorAll('.dsdata-delete-kebab');
|
||||
deleteKebabButtons.forEach(deleteKebabButton => {
|
||||
deleteKebabButton.addEventListener('click', this.handleDeleteKebabClick);
|
||||
});
|
||||
|
||||
const inputs = document.querySelectorAll("input[type='text'], textarea");
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener("input", () => {
|
||||
this.formChanged = true;
|
||||
});
|
||||
});
|
||||
|
||||
const selects = document.querySelectorAll("select");
|
||||
selects.forEach(select => {
|
||||
select.addEventListener("change", () => {
|
||||
this.formChanged = true;
|
||||
});
|
||||
});
|
||||
|
||||
// Set event listeners on the submit buttons for the modals. Event listeners
|
||||
// should execute the callback function, which has its logic updated prior
|
||||
// to modal display
|
||||
const unsaved_changes_modal = document.getElementById('unsaved-changes-modal');
|
||||
if (unsaved_changes_modal) {
|
||||
const submitButton = document.getElementById('unsaved-changes-click-button');
|
||||
const closeButton = unsaved_changes_modal.querySelector('.usa-modal__close');
|
||||
submitButton.addEventListener('click', () => {
|
||||
closeButton.click();
|
||||
this.executeCallback();
|
||||
});
|
||||
}
|
||||
const cancel_changes_modal = document.getElementById('cancel-changes-modal');
|
||||
if (cancel_changes_modal) {
|
||||
const submitButton = document.getElementById('cancel-changes-click-button');
|
||||
const closeButton = cancel_changes_modal.querySelector('.usa-modal__close');
|
||||
submitButton.addEventListener('click', () => {
|
||||
closeButton.click();
|
||||
this.executeCallback();
|
||||
});
|
||||
}
|
||||
const delete_modal = document.getElementById('delete-modal');
|
||||
if (delete_modal) {
|
||||
const submitButton = document.getElementById('delete-click-button');
|
||||
const closeButton = delete_modal.querySelector('.usa-modal__close');
|
||||
submitButton.addEventListener('click', () => {
|
||||
closeButton.click();
|
||||
this.executeCallback();
|
||||
});
|
||||
}
|
||||
const disable_dnssec_modal = document.getElementById('disable-dnssec-modal');
|
||||
if (disable_dnssec_modal) {
|
||||
const submitButton = document.getElementById('disable-dnssec-click-button');
|
||||
const closeButton = disable_dnssec_modal.querySelector('.usa-modal__close');
|
||||
submitButton.addEventListener('click', () => {
|
||||
closeButton.click();
|
||||
this.executeCallback();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a stored callback function if defined, otherwise logs a warning.
|
||||
*/
|
||||
executeCallback() {
|
||||
if (this.callback) {
|
||||
this.callback();
|
||||
this.callback = null;
|
||||
} else {
|
||||
console.warn("No callback function set.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking the 'Add DS data' button, showing the form if needed.
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleAddFormClick(event) {
|
||||
this.callback = () => {
|
||||
// Check if any other edit row is currently visible and hide it
|
||||
document.querySelectorAll('tr.edit-row:not(.display-none)').forEach(openEditRow => {
|
||||
this.resetEditRowAndFormAndCollapseEditRow(openEditRow);
|
||||
});
|
||||
if (this.addDSDataForm) {
|
||||
// Check if this.addDSDataForm is visible (i.e., does not have 'display-none')
|
||||
if (!this.addDSDataForm.classList.contains('display-none')) {
|
||||
this.resetAddDSDataForm();
|
||||
}
|
||||
// show add ds data form
|
||||
showElement(this.addDSDataForm);
|
||||
// focus on key tag in the form
|
||||
let keyTagInput = this.addDSDataForm.querySelector('input[name$="-key_tag"]');
|
||||
if (keyTagInput) {
|
||||
keyTagInput.focus();
|
||||
}
|
||||
} else {
|
||||
this.addAlert("error", "You’ve reached the maximum amount of DS Data records (8). To add another record, you’ll need to delete one of your saved records.");
|
||||
}
|
||||
};
|
||||
if (this.formChanged) {
|
||||
//------- Show the unsaved changes confirmation modal
|
||||
let modalTrigger = document.querySelector("#unsaved_changes_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
this.executeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking an 'Edit' button on a readonly row, which hides the readonly row
|
||||
* and displays the edit row, after performing some checks and possibly displaying modal.
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleEditClick(event) {
|
||||
let editButton = event.target;
|
||||
let readOnlyRow = editButton.closest('tr'); // Find the closest row
|
||||
let editRow = readOnlyRow.nextElementSibling; // Get the next row
|
||||
if (!editRow || !readOnlyRow) {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
return;
|
||||
}
|
||||
this.callback = () => {
|
||||
// Check if any other edit row is currently visible and hide it
|
||||
document.querySelectorAll('tr.edit-row:not(.display-none)').forEach(openEditRow => {
|
||||
this.resetEditRowAndFormAndCollapseEditRow(openEditRow);
|
||||
});
|
||||
// Check if this.addDSDataForm is visible (i.e., does not have 'display-none')
|
||||
if (this.addDSDataForm && !this.addDSDataForm.classList.contains('display-none')) {
|
||||
this.resetAddDSDataForm();
|
||||
}
|
||||
// hide and show rows as appropriate
|
||||
hideElement(readOnlyRow);
|
||||
showElement(editRow);
|
||||
};
|
||||
if (this.formChanged) {
|
||||
//------- Show the unsaved changes confirmation modal
|
||||
let modalTrigger = document.querySelector("#unsaved_changes_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
this.executeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking a 'Delete' button on an edit row, which hattempts to delete the DS record
|
||||
* after displaying modal.
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleDeleteClick(event) {
|
||||
let deleteButton = event.target;
|
||||
let editRow = deleteButton.closest('tr');
|
||||
if (!editRow) {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
return;
|
||||
}
|
||||
this.deleteRow(editRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking a 'Delete' button on a readonly row in a kebab, which attempts to delete the DS record
|
||||
* after displaying modal.
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleDeleteKebabClick(event) {
|
||||
let deleteKebabButton = event.target;
|
||||
let accordionDiv = deleteKebabButton.closest('div');
|
||||
// hide the accordion
|
||||
accordionDiv.hidden = true;
|
||||
let readOnlyRow = deleteKebabButton.closest('tr'); // Find the closest row
|
||||
let editRow = readOnlyRow.nextElementSibling; // Get the next row
|
||||
if (!editRow) {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
return;
|
||||
}
|
||||
this.deleteRow(editRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a DS record row. If there is only one DS record, prompt the user
|
||||
* that they will be disabling DNSSEC. Otherwise, prompt with delete confiration.
|
||||
* If deletion proceeds, the input fields are cleared, and the form is submitted.
|
||||
* @param {HTMLElement} editRow - The row corresponding to the DS record being deleted.
|
||||
*/
|
||||
deleteRow(editRow) {
|
||||
// update the callback method
|
||||
this.callback = () => {
|
||||
hideElement(editRow);
|
||||
let deleteInput = editRow.querySelector("input[name$='-DELETE']");
|
||||
if (deleteInput) {
|
||||
deleteInput.checked = true;
|
||||
}
|
||||
document.querySelector("form").submit();
|
||||
};
|
||||
// Check if at least 2 DS data records exist before the delete row action is taken
|
||||
const thirdDSData = document.getElementById('id_form-2-key_tag')
|
||||
if (thirdDSData) {
|
||||
let modalTrigger = document.querySelector('#delete_trigger');
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
let modalTrigger = document.querySelector('#disable_dnssec_trigger');
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event on the "Cancel" button in the add DS data form.
|
||||
* Resets the form fields and hides the add form section.
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleCancelAddFormClick(event) {
|
||||
this.callback = () => {
|
||||
this.resetAddDSDataForm();
|
||||
}
|
||||
if (this.formChanged) {
|
||||
// Show the cancel changes confirmation modal
|
||||
let modalTrigger = document.querySelector("#cancel_changes_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
this.executeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event for the cancel button within the table form.
|
||||
*
|
||||
* This method identifies the edit row containing the cancel button and resets
|
||||
* it to its initial state, restoring the corresponding read-only row.
|
||||
*
|
||||
* @param {Event} event - the click event triggered by the cancel button
|
||||
*/
|
||||
handleCancelClick(event) {
|
||||
// get the cancel button that was clicked
|
||||
let cancelButton = event.target;
|
||||
// find the closest table row that contains the cancel button
|
||||
let editRow = cancelButton.closest('tr');
|
||||
this.callback = () => {
|
||||
if (editRow) {
|
||||
this.resetEditRowAndFormAndCollapseEditRow(editRow);
|
||||
} else {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
}
|
||||
}
|
||||
if (this.formChanged) {
|
||||
// Show the cancel changes confirmation modal
|
||||
let modalTrigger = document.querySelector("#cancel_changes_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
this.executeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the edit row, restores its original values, removes validation errors,
|
||||
* and collapses the edit row while making the readonly row visible again.
|
||||
* @param {HTMLElement} editRow - The row that is being reset and collapsed.
|
||||
*/
|
||||
resetEditRowAndFormAndCollapseEditRow(editRow) {
|
||||
let readOnlyRow = editRow.previousElementSibling; // Get the next row
|
||||
if (!editRow || !readOnlyRow) {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
return;
|
||||
}
|
||||
// reset the values set in editRow
|
||||
this.resetInputValuesInElement(editRow);
|
||||
// copy values from editRow to readOnlyRow
|
||||
this.copyEditRowToReadonlyRow(editRow, readOnlyRow);
|
||||
// remove errors from the editRow
|
||||
removeErrorsFromElement(editRow);
|
||||
// remove errors from the entire form
|
||||
removeFormErrors();
|
||||
// reset formChanged
|
||||
this.resetFormChanged();
|
||||
// hide and show rows as appropriate
|
||||
hideElement(editRow);
|
||||
showElement(readOnlyRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the 'Add DS data' form by clearing its input fields, removing errors,
|
||||
* and hiding the form to return it to its initial state.
|
||||
*/
|
||||
resetAddDSDataForm() {
|
||||
if (this.addDSDataForm) {
|
||||
// reset the values set in addDSDataForm
|
||||
this.resetInputValuesInElement(this.addDSDataForm);
|
||||
// remove errors from the addDSDataForm
|
||||
removeErrorsFromElement(this.addDSDataForm);
|
||||
// remove errors from the entire form
|
||||
removeFormErrors();
|
||||
// reset formChanged
|
||||
this.resetFormChanged();
|
||||
// hide the addDSDataForm
|
||||
hideElement(this.addDSDataForm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all text input fields within the specified DOM element to their initial values.
|
||||
* Triggers an 'input' event to ensure any event listeners update accordingly.
|
||||
* @param {HTMLElement} domElement - The parent element containing text input fields to be reset.
|
||||
*/
|
||||
resetInputValuesInElement(domElement) {
|
||||
const inputEvent = new Event('input');
|
||||
const changeEvent = new Event('change');
|
||||
// Reset text inputs
|
||||
const inputs = document.querySelectorAll("input[type='text'], textarea");
|
||||
inputs.forEach(input => {
|
||||
// Reset input value to its initial stored value
|
||||
input.value = input.dataset.initialValue;
|
||||
// Dispatch input event to update any event-driven changes
|
||||
input.dispatchEvent(inputEvent);
|
||||
});
|
||||
// Reset select elements
|
||||
let selects = domElement.querySelectorAll("select");
|
||||
selects.forEach(select => {
|
||||
// Reset select value to its initial stored value
|
||||
select.value = select.dataset.initialValue;
|
||||
// Dispatch change event to update any event-driven changes
|
||||
select.dispatchEvent(changeEvent);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies values from the editable row's text inputs into the corresponding
|
||||
* readonly row cells, formatting them appropriately.
|
||||
* @param {HTMLElement} editRow - The row containing editable input fields.
|
||||
* @param {HTMLElement} readOnlyRow - The row where values will be displayed in a non-editable format.
|
||||
*/
|
||||
copyEditRowToReadonlyRow(editRow, readOnlyRow) {
|
||||
let keyTagInput = editRow.querySelector("input[type='text']");
|
||||
let selects = editRow.querySelectorAll("select");
|
||||
let digestInput = editRow.querySelector("textarea");
|
||||
let tds = readOnlyRow.querySelectorAll("td");
|
||||
|
||||
// Copy the key tag input value
|
||||
if (keyTagInput) {
|
||||
tds[0].innerText = keyTagInput.value || "";
|
||||
}
|
||||
|
||||
// Copy select values (showing the selected label instead of value)
|
||||
if (selects[0]) {
|
||||
let selectedOption = selects[0].options[selects[0].selectedIndex];
|
||||
if (tds[1]) {
|
||||
tds[1].innerHTML = `<span class="ellipsis ellipsis--15">${selectedOption ? selectedOption.text : ""}</span>`;
|
||||
}
|
||||
}
|
||||
if (selects[1]) {
|
||||
let selectedOption = selects[1].options[selects[1].selectedIndex];
|
||||
if (tds[2]) {
|
||||
tds[2].innerText = selectedOption ? selectedOption.text : "";
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the digest input value
|
||||
if (digestInput) {
|
||||
tds[3].innerHTML = `<span class="ellipsis ellipsis--23">${digestInput.value || ""}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the form change state.
|
||||
* This method marks the form as unchanged by setting `formChanged` to false.
|
||||
* It is useful for tracking whether a user has modified any form fields.
|
||||
*/
|
||||
resetFormChanged() {
|
||||
this.formChanged = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all existing alert messages from the main content area.
|
||||
* This ensures that only the latest alert is displayed to the user.
|
||||
*/
|
||||
resetAlerts() {
|
||||
const mainContent = document.getElementById("main-content");
|
||||
if (mainContent) {
|
||||
// Remove all alert elements within the main content area
|
||||
mainContent.querySelectorAll(".usa-alert:not(.usa-alert--do-not-reset)").forEach(alert => alert.remove());
|
||||
} else {
|
||||
console.warn("Expecting main-content DOM element");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an alert message at the top of the main content area.
|
||||
* It first removes any existing alerts before adding a new one to ensure only the latest alert is visible.
|
||||
* @param {string} level - The alert level (e.g., 'error', 'success', 'warning', 'info').
|
||||
* @param {string} message - The message to display inside the alert.
|
||||
*/
|
||||
addAlert(level, message) {
|
||||
this.resetAlerts(); // Remove any existing alerts before adding a new one
|
||||
|
||||
const mainContent = document.getElementById("main-content");
|
||||
if (!mainContent) return;
|
||||
|
||||
// Create a new alert div with appropriate classes based on alert level
|
||||
const alertDiv = document.createElement("div");
|
||||
alertDiv.className = `usa-alert usa-alert--${level} usa-alert--slim margin-bottom-2`;
|
||||
alertDiv.setAttribute("role", "alert"); // Add the role attribute
|
||||
|
||||
// Create the alert body to hold the message text
|
||||
const alertBody = document.createElement("div");
|
||||
alertBody.className = "usa-alert__body";
|
||||
alertBody.textContent = message;
|
||||
|
||||
// Append the alert body to the alert div and insert it at the top of the main content area
|
||||
alertDiv.appendChild(alertBody);
|
||||
mainContent.insertBefore(alertDiv, mainContent.firstChild);
|
||||
|
||||
// Scroll the page to make the alert visible to the user
|
||||
scrollToElement("class", "usa-alert__body");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the DSDataForm when the DOM is fully loaded.
|
||||
*/
|
||||
export function initFormDSData() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.getElementById('dsdata-add-button')) {
|
||||
const dsDataForm = new DSDataForm();
|
||||
dsDataForm.init();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -38,6 +38,16 @@ export function removeErrorsFromElement(domElement) {
|
|||
domElement.querySelectorAll("input.usa-input--error").forEach(input => {
|
||||
input.classList.remove("usa-input--error");
|
||||
});
|
||||
|
||||
// Remove the 'usa-input--error' class from all select elements
|
||||
domElement.querySelectorAll("select.usa-input--error").forEach(select => {
|
||||
select.classList.remove("usa-input--error");
|
||||
});
|
||||
|
||||
// Remove the 'usa-input--error' class from all textarea elements
|
||||
domElement.querySelectorAll("textarea.usa-input--error").forEach(textarea => {
|
||||
textarea.classList.remove("usa-input--error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -176,6 +176,15 @@ export class NameserverForm {
|
|||
this.executeCallback();
|
||||
});
|
||||
}
|
||||
const cancel_changes_modal = document.getElementById('cancel-changes-modal');
|
||||
if (cancel_changes_modal) {
|
||||
const submitButton = document.getElementById('cancel-changes-click-button');
|
||||
const closeButton = cancel_changes_modal.querySelector('.usa-modal__close');
|
||||
submitButton.addEventListener('click', () => {
|
||||
closeButton.click();
|
||||
this.executeCallback();
|
||||
});
|
||||
}
|
||||
const delete_modal = document.getElementById('delete-modal');
|
||||
if (delete_modal) {
|
||||
const submitButton = document.getElementById('delete-click-button');
|
||||
|
@ -338,7 +347,18 @@ export class NameserverForm {
|
|||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleCancelAddFormClick(event) {
|
||||
this.resetAddNameserversForm();
|
||||
this.callback = () => {
|
||||
this.resetAddNameserversForm();
|
||||
}
|
||||
if (this.formChanged) {
|
||||
// Show the cancel changes confirmation modal
|
||||
let modalTrigger = document.querySelector("#cancel_changes_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
this.executeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -354,10 +374,21 @@ export class NameserverForm {
|
|||
let cancelButton = event.target;
|
||||
// find the closest table row that contains the cancel button
|
||||
let editRow = cancelButton.closest('tr');
|
||||
if (editRow) {
|
||||
this.resetEditRowAndFormAndCollapseEditRow(editRow);
|
||||
this.callback = () => {
|
||||
if (editRow) {
|
||||
this.resetEditRowAndFormAndCollapseEditRow(editRow);
|
||||
} else {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
}
|
||||
}
|
||||
if (this.formChanged) {
|
||||
// Show the cancel changes confirmation modal
|
||||
let modalTrigger = document.querySelector("#cancel_changes_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
this.executeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ function markForm(e, formLabel){
|
|||
}
|
||||
|
||||
/**
|
||||
* Prepare the namerservers, DS data and Other Contacts formsets' delete button
|
||||
* Prepare the Other Contacts formsets' delete button
|
||||
* for the last added form. We call this from the Add function
|
||||
*
|
||||
*/
|
||||
|
@ -108,7 +108,7 @@ function prepareNewDeleteButton(btn, formLabel) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Prepare the namerservers, DS data and Other Contacts formsets' delete buttons
|
||||
* Prepare the Other Contacts formsets' delete buttons
|
||||
* We will call this on the forms init
|
||||
*
|
||||
*/
|
||||
|
@ -172,16 +172,11 @@ export function initFormsetsForms() {
|
|||
let cloneIndex = 0;
|
||||
let formLabel = '';
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let isDsDataForm = document.querySelector(".ds-data-form");
|
||||
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
|
||||
if( !(isOtherContactsForm || isDotgovDomain || isDsDataForm) ){
|
||||
if( !(isOtherContactsForm || isDotgovDomain) ){
|
||||
return
|
||||
}
|
||||
// DNSSEC: DS Data
|
||||
if (isDsDataForm) {
|
||||
formLabel = "DS data record";
|
||||
// The Other Contacts form
|
||||
} else if (isOtherContactsForm) {
|
||||
if (isOtherContactsForm) {
|
||||
formLabel = "Organization contact";
|
||||
container = document.querySelector("#other-employees");
|
||||
formIdentifier = "other_contacts"
|
||||
|
@ -287,26 +282,3 @@ export function initFormsetsForms() {
|
|||
prepareNewDeleteButton(newDeleteButton, formLabel);
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerModalOnDsDataForm() {
|
||||
let saveButon = document.querySelector("#save-ds-data");
|
||||
|
||||
// The view context will cause a hitherto hidden modal trigger to
|
||||
// show up. On save, we'll test for that modal trigger appearing. We'll
|
||||
// run that test once every 100 ms for 5 secs, which should balance performance
|
||||
// while accounting for network or lag issues.
|
||||
if (saveButon) {
|
||||
let i = 0;
|
||||
var tryToTriggerModal = setInterval(function() {
|
||||
i++;
|
||||
if (i > 100) {
|
||||
clearInterval(tryToTriggerModal);
|
||||
}
|
||||
let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click()
|
||||
clearInterval(tryToTriggerModal);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js';
|
||||
import { initDomainValidators } from './domain-validators.js';
|
||||
import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js';
|
||||
import { initFormNameservers } from './form-nameservers'
|
||||
import { initFormsetsForms } from './formset-forms.js';
|
||||
import { initFormNameservers } from './form-nameservers';
|
||||
import { initFormDSData } from './form-dsdata.js';
|
||||
import { initializeUrbanizationToggle } from './urbanization.js';
|
||||
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
||||
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
||||
|
@ -13,7 +14,6 @@ import { initEditMemberDomainsTable } from './table-edit-member-domains.js';
|
|||
import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js';
|
||||
import { initDomainRequestForm } from './domain-request-form.js';
|
||||
import { initDomainManagersPage } from './domain-managers.js';
|
||||
import { initDomainDSData } from './domain-dsdata.js';
|
||||
import { initDomainDNSSEC } from './domain-dnssec.js';
|
||||
import { initFormErrorHandling } from './form-errors.js';
|
||||
import { domain_purpose_choice_callbacks } from './domain-purpose-form.js';
|
||||
|
@ -22,12 +22,14 @@ import { initButtonLinks } from '../getgov-admin/button-utils.js';
|
|||
initDomainValidators();
|
||||
|
||||
initFormsetsForms();
|
||||
triggerModalOnDsDataForm();
|
||||
initFormNameservers();
|
||||
initFormDSData();
|
||||
|
||||
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
|
||||
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
|
||||
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
|
||||
hookupYesNoListener("portfolio_additional_details-working_with_eop", "eop-contact-container", null);
|
||||
hookupYesNoListener("portfolio_additional_details-has_anything_else_text", 'anything-else-details-container', null);
|
||||
hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
|
||||
|
||||
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
|
||||
|
@ -35,7 +37,6 @@ hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choic
|
|||
hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
|
||||
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
|
||||
|
||||
|
||||
initializeUrbanizationToggle();
|
||||
|
||||
userProfileListener();
|
||||
|
@ -51,7 +52,6 @@ initEditMemberDomainsTable();
|
|||
|
||||
initDomainRequestForm();
|
||||
initDomainManagersPage();
|
||||
initDomainDSData();
|
||||
initDomainDNSSEC();
|
||||
|
||||
initFormErrorHandling();
|
||||
|
|
|
@ -103,7 +103,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
|||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions margin-top-2px"
|
||||
aria-expanded="false"
|
||||
aria-controls="more-actions-${unique_id}"
|
||||
aria-label="${screen_reader_text}"
|
||||
|
|
|
@ -74,7 +74,7 @@ export class DomainRequestsTable extends BaseTable {
|
|||
|
||||
if (this.portfolioValue) {
|
||||
markupCreatorRow = `
|
||||
<td>
|
||||
<td data-label="Created by">
|
||||
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
|
||||
</td>
|
||||
`
|
||||
|
@ -117,7 +117,7 @@ export class DomainRequestsTable extends BaseTable {
|
|||
<td data-label="Status">
|
||||
${request.status}
|
||||
</td>
|
||||
<td class="width--action-column">
|
||||
<td data-label="Action" class="width--action-column">
|
||||
<div class="tablet:display-flex tablet:flex-row">
|
||||
<a href="${actionUrl}" ${customTableOptions.hasAdditionalActions ? "class='margin-right-2'" : ''}>
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
|
|
|
@ -27,7 +27,7 @@ export class DomainsTable extends BaseTable {
|
|||
|
||||
if (this.portfolioValue) {
|
||||
markupForSuborganizationRow = `
|
||||
<td>
|
||||
<td data-label="Suborganization">
|
||||
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
||||
</td>
|
||||
`
|
||||
|
@ -56,7 +56,7 @@ export class DomainsTable extends BaseTable {
|
|||
</svg>
|
||||
</td>
|
||||
${markupForSuborganizationRow}
|
||||
<td class="width--action-column">
|
||||
<td data-label="Action" class="width--action-column">
|
||||
<div class="tablet:display-flex tablet:flex-row flex-align-center margin-right-2">
|
||||
<a href="${actionUrl}">
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
|
|
|
@ -116,7 +116,7 @@ export class MembersTable extends BaseTable {
|
|||
<td class="padding-bottom-0" headers="header-last-active row-header-${unique_id}" data-sort-value="${last_active.sort_value}" data-label="Last active">
|
||||
${last_active.display_value}
|
||||
</td>
|
||||
<td class="padding-bottom-0" headers="header-action row-header-${unique_id}" class="width--action-column">
|
||||
<td data-label="Action" class="padding-bottom-0" headers="header-action row-header-${unique_id}" class="width--action-column">
|
||||
<div class="tablet:display-flex tablet:flex-row flex-align-center">
|
||||
<a href="${member.action_url}" ${customTableOptions.hasAdditionalActions ? "class='margin-right-2'" : ''}>
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
|
|
|
@ -39,6 +39,9 @@
|
|||
.margin-top-0 {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.margin-top-2px {
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// This will work in responsive tables if we overwrite the overflow value on the table container
|
||||
|
@ -67,10 +70,11 @@
|
|||
// Currently, that's not an issue since that Members table is not wrapped in the
|
||||
// reponsive wrapper.
|
||||
@include at-media-max("desktop") {
|
||||
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
|
||||
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content,
|
||||
tr.view-only-row:nth-last-of-type(2) .usa-accordion--more-actions .usa-accordion__content {
|
||||
top: auto;
|
||||
bottom: -10px;
|
||||
right: 30px;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,10 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.section-outlined--extra-padding {
|
||||
padding: units(2) units(3) units(3);
|
||||
}
|
||||
|
||||
.section-outlined--border-base-light {
|
||||
border: 1px solid color('base-light');
|
||||
}
|
||||
|
@ -217,6 +221,10 @@ abbr[title] {
|
|||
max-width: 23ch;
|
||||
}
|
||||
|
||||
.ellipsis--15 {
|
||||
max-width: 15ch;
|
||||
}
|
||||
|
||||
.vertical-align-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,11 @@
|
|||
padding-left: $widescreen-x-padding !important;
|
||||
padding-right: $widescreen-x-padding !important;
|
||||
}
|
||||
|
||||
// Accomodate sideanv + table layouts
|
||||
.grid-col--sidenav {
|
||||
max-width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
// matches max-width to equal the max-width of .grid-container
|
||||
|
@ -22,5 +27,5 @@
|
|||
// regular grid-container within a widescreen (see instances
|
||||
// where is_widescreen_centered is used in the html).
|
||||
.max-width--grid-container {
|
||||
max-width: 960px;
|
||||
}
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
|
|
@ -79,3 +79,14 @@ legend.float-left-tablet + button.float-right-tablet {
|
|||
.bg-gray-1 .usa-radio {
|
||||
background: color('gray-1');
|
||||
}
|
||||
|
||||
.usa-textarea--digest {
|
||||
max-height: 4rem;
|
||||
min-width: 13rem;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.usa-form .usa-button.margin-top-2 {
|
||||
margin-top: units(2) !important;
|
||||
}
|
||||
|
|
|
@ -142,6 +142,14 @@ th {
|
|||
}
|
||||
}
|
||||
|
||||
.dotgov-table--cell-padding-2-2-2-0 {
|
||||
@include at-media(mobile-lg) {
|
||||
td, th {
|
||||
padding: units(2) units(2) units(2) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usa-table--striped tbody tr:nth-child(odd) th,
|
||||
.usa-table--striped tbody tr:nth-child(odd) td {
|
||||
background-color: color('primary-lightest');
|
||||
|
@ -165,4 +173,8 @@ th {
|
|||
.usa-table-container--scrollable.usa-table-container--override-overflow {
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.usa-table-container--override-scrollable td {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -203,6 +203,8 @@ MIDDLEWARE = [
|
|||
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||
# Restrict access using Opt-Out approach
|
||||
"registrar.registrar_middleware.RestrictAccessMiddleware",
|
||||
# Our own router logs that included user info to speed up log tracing time on stable
|
||||
"registrar.registrar_middleware.RequestLoggingMiddleware",
|
||||
]
|
||||
|
||||
# application object used by Django's built-in servers (e.g. `runserver`)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import logging
|
||||
from django import forms
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||
from django.forms import formset_factory
|
||||
from registrar.forms.utility.combobox import ComboboxWidget
|
||||
from registrar.models import DomainRequest, FederalAgency
|
||||
|
@ -630,14 +630,10 @@ class DomainDsdataForm(forms.Form):
|
|||
if not re.match(r"^[0-9a-fA-F]+$", value):
|
||||
raise forms.ValidationError(str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)))
|
||||
|
||||
key_tag = forms.IntegerField(
|
||||
key_tag = forms.CharField(
|
||||
required=True,
|
||||
label="Key tag",
|
||||
validators=[
|
||||
MinValueValidator(0, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))),
|
||||
MaxValueValidator(65535, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))),
|
||||
],
|
||||
error_messages={"required": ("Key tag is required.")},
|
||||
error_messages={"required": "Key tag is required."},
|
||||
)
|
||||
|
||||
algorithm = forms.TypedChoiceField(
|
||||
|
@ -663,6 +659,13 @@ class DomainDsdataForm(forms.Form):
|
|||
error_messages={
|
||||
"required": "Digest is required.",
|
||||
},
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"maxlength": "64",
|
||||
"class": "text-wrap usa-textarea--digest",
|
||||
"hide_character_count": "True",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
@ -672,6 +675,22 @@ class DomainDsdataForm(forms.Form):
|
|||
cleaned_data = super().clean()
|
||||
digest_type = cleaned_data.get("digest_type", 0)
|
||||
digest = cleaned_data.get("digest", "")
|
||||
|
||||
# Convert key_tag to an integer safely
|
||||
key_tag = cleaned_data.get("key_tag", 0)
|
||||
try:
|
||||
key_tag = int(key_tag)
|
||||
if key_tag < 0 or key_tag > 65535:
|
||||
self.add_error(
|
||||
"key_tag",
|
||||
DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE),
|
||||
)
|
||||
except ValueError:
|
||||
self.add_error(
|
||||
"key_tag",
|
||||
DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_CHARS),
|
||||
)
|
||||
|
||||
# validate length of digest depending on digest_type
|
||||
if digest_type == 1 and len(digest) != 40:
|
||||
self.add_error(
|
||||
|
@ -686,9 +705,45 @@ class DomainDsdataForm(forms.Form):
|
|||
return cleaned_data
|
||||
|
||||
|
||||
class BaseDsdataFormset(forms.BaseFormSet):
|
||||
def clean(self):
|
||||
"""Check for duplicate entries in the formset."""
|
||||
if any(self.errors):
|
||||
return # Skip duplicate checking if other errors exist
|
||||
|
||||
duplicate_errors = self._check_for_duplicates()
|
||||
if duplicate_errors:
|
||||
raise forms.ValidationError("Duplicate DS records found. Each DS record must be unique.")
|
||||
|
||||
def _check_for_duplicates(self):
|
||||
"""Check for duplicate entries in the DS data forms"""
|
||||
|
||||
seen_ds_records = set()
|
||||
duplicate_found = False
|
||||
|
||||
for form in self.forms:
|
||||
if form.cleaned_data.get("key_tag") and not form.cleaned_data.get("DELETE", False):
|
||||
ds_tuple = (
|
||||
form.cleaned_data["key_tag"],
|
||||
form.cleaned_data["algorithm"],
|
||||
form.cleaned_data["digest_type"],
|
||||
form.cleaned_data["digest"].upper(),
|
||||
)
|
||||
|
||||
if ds_tuple in seen_ds_records:
|
||||
form.add_error("key_tag", "You already entered this DS record. DS records must be unique.")
|
||||
duplicate_found = True # Track that we found at least one duplicate
|
||||
|
||||
seen_ds_records.add(ds_tuple)
|
||||
|
||||
return duplicate_found # Returns True if any duplicates were found
|
||||
|
||||
|
||||
DomainDsdataFormset = formset_factory(
|
||||
DomainDsdataForm,
|
||||
extra=0,
|
||||
formset=BaseDsdataFormset,
|
||||
extra=1,
|
||||
max_num=8,
|
||||
can_delete=True,
|
||||
)
|
||||
|
||||
|
|
|
@ -615,7 +615,8 @@ class PurposeDetailsForm(BaseDeletableRegistrarForm):
|
|||
label="Purpose",
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"aria-label": "What is the purpose of your requested domain? Describe how you’ll use your .gov domain. \
|
||||
"aria-label": "What is the purpose of your requested domain? \
|
||||
Describe how you’ll use your .gov domain. \
|
||||
Will it be used for a website, email, or something else?"
|
||||
}
|
||||
),
|
||||
|
@ -921,6 +922,7 @@ class AnythingElseYesNoForm(BaseYesNoForm):
|
|||
|
||||
|
||||
class RequirementsForm(RegistrarForm):
|
||||
|
||||
is_policy_acknowledged = forms.BooleanField(
|
||||
label="I read and agree to the requirements for operating a .gov domain.",
|
||||
error_messages={
|
||||
|
|
|
@ -121,3 +121,83 @@ class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
|
|||
],
|
||||
error_messages={"required": "Name the agencies that will be involved in this initiative."},
|
||||
)
|
||||
|
||||
|
||||
class WorkingWithEOPYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
|
||||
"""
|
||||
Form for determining if the Federal Executive Branch (FEB) agency is working with the
|
||||
Executive Office of the President (EOP) on the domain request.
|
||||
"""
|
||||
|
||||
field_name = "working_with_eop"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||
"""
|
||||
return self.domain_request.working_with_eop
|
||||
|
||||
|
||||
class EOPContactForm(BaseDeletableRegistrarForm):
|
||||
"""
|
||||
Form for contact information of the representative of the
|
||||
Executive Office of the President (EOP) that the Federal
|
||||
Executive Branch (FEB) agency is working with.
|
||||
"""
|
||||
|
||||
first_name = forms.CharField(
|
||||
label="First name / given name",
|
||||
error_messages={"required": "Enter the first name / given name of this contact."},
|
||||
required=True,
|
||||
)
|
||||
last_name = forms.CharField(
|
||||
label="Last name / family name",
|
||||
error_messages={"required": "Enter the last name / family name of this contact."},
|
||||
required=True,
|
||||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={
|
||||
"required": ("Enter an email address in the required format, like name@example.com."),
|
||||
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
||||
},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
required=True,
|
||||
help_text="Enter an email address in the required format, like name@example.com.",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
return {
|
||||
"first_name": obj.eop_stakeholder_first_name,
|
||||
"last_name": obj.eop_stakeholder_last_name,
|
||||
"email": obj.eop_stakeholder_email,
|
||||
}
|
||||
|
||||
def to_database(self, obj):
|
||||
# This function overrides the behavior of the BaseDeletableRegistrarForm.
|
||||
# in order to preserve deletable functionality, we need to call the
|
||||
# superclass's to_database method if the form is marked for deletion.
|
||||
if self.form_data_marked_for_deletion:
|
||||
super().to_database(obj)
|
||||
return
|
||||
if not self.is_valid():
|
||||
return
|
||||
obj.eop_stakeholder_first_name = self.cleaned_data["first_name"]
|
||||
obj.eop_stakeholder_last_name = self.cleaned_data["last_name"]
|
||||
obj.eop_stakeholder_email = self.cleaned_data["email"]
|
||||
obj.save()
|
||||
|
||||
|
||||
class FEBAnythingElseYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm):
|
||||
"""Yes/no toggle for the anything else question on additional details"""
|
||||
|
||||
form_is_checked = property(lambda self: self.domain_request.has_anything_else_text) # type: ignore
|
||||
field_name = "has_anything_else_text"
|
||||
|
|
|
@ -60,6 +60,7 @@ class UserProfileForm(forms.ModelForm):
|
|||
self.fields["email"].error_messages = {
|
||||
"required": "Enter an email address in the required format, like name@example.com."
|
||||
}
|
||||
self.fields["email"].widget.attrs["hide_character_count"] = "True"
|
||||
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||
|
||||
if self.instance and self.instance.phone:
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import logging
|
||||
import argparse
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||
from registrar.models import PublicContact
|
||||
from registrar.models.utility.generic_helper import normalize_string
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand, PopulateScriptTemplate):
|
||||
help = "Loops through each default PublicContact and updates some values on each"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Adds command line arguments"""
|
||||
parser.add_argument(
|
||||
"--overwrite_updated_contacts",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help=(
|
||||
"Loops over PublicContacts with an email of 'help@get.gov' when enabled."
|
||||
"Use this setting if the record was updated in the DB but not correctly in EPP."
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--target_domain",
|
||||
help=(
|
||||
"Updates the public contact on a given domain name (case insensitive). "
|
||||
"Use this option to avoid doing a mass-update of every public contact record."
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, **kwargs):
|
||||
"""Loops through each valid User object and updates its verification_type value"""
|
||||
overwrite_updated_contacts = kwargs.get("overwrite_updated_contacts")
|
||||
target_domain = kwargs.get("target_domain")
|
||||
default_emails = {email for email in DefaultEmail}
|
||||
|
||||
# Don't update records we've already updated
|
||||
if not overwrite_updated_contacts:
|
||||
default_emails.remove(DefaultEmail.PUBLIC_CONTACT_DEFAULT)
|
||||
|
||||
# We should only update DEFAULT records. This means that if all values are not default,
|
||||
# we should skip as this could lead to data corruption.
|
||||
# Since we check for all fields, we don't account for casing differences.
|
||||
self.old_and_new_default_contact_values = {
|
||||
"name": {
|
||||
"csd/cb – attn: .gov tld",
|
||||
"csd/cb – attn: cameron dixon",
|
||||
"program manager",
|
||||
"registry customer service",
|
||||
},
|
||||
"street1": {"1110 n. glebe rd", "cisa – ngr stop 0645", "4200 wilson blvd."},
|
||||
"pc": {"22201", "20598-0645"},
|
||||
"email": default_emails,
|
||||
}
|
||||
if not target_domain:
|
||||
filter_condition = {"email__in": default_emails}
|
||||
else:
|
||||
filter_condition = {"email__in": default_emails, "domain__name__iexact": target_domain}
|
||||
# This variable is decorative since we are skipping bulk update
|
||||
fields_to_update = ["name", "street1", "pc", "email"]
|
||||
self.mass_update_records(PublicContact, filter_condition, fields_to_update, show_record_count=True)
|
||||
|
||||
def bulk_update_fields(self, *args, **kwargs):
|
||||
"""Skip bulk update since we need to manually save each field.
|
||||
Our EPP logic is tied to an override of .save(), and this also associates
|
||||
with our caching logic for this area of the code.
|
||||
|
||||
Since bulk update does not trigger .save() for each field, we have to
|
||||
call it manually.
|
||||
"""
|
||||
return None
|
||||
|
||||
def update_record(self, record: PublicContact):
|
||||
"""Defines how we update the verification_type field"""
|
||||
record.name = "CSD/CB – Attn: .gov TLD"
|
||||
record.street1 = "1110 N. Glebe Rd"
|
||||
record.pc = "22201"
|
||||
record.email = DefaultEmail.PUBLIC_CONTACT_DEFAULT
|
||||
record.save()
|
||||
logger.info(f"{TerminalColors.OKCYAN}Updated '{record}' in EPP.{TerminalColors.ENDC}")
|
||||
|
||||
def should_skip_record(self, record) -> bool: # noqa
|
||||
"""Skips updating a public contact if it contains different default info."""
|
||||
if record.registry_id and len(record.registry_id) < 16:
|
||||
message = (
|
||||
f"Skipping legacy verisign contact '{record}'. "
|
||||
f"The registry_id field has a length less than 16 characters."
|
||||
)
|
||||
logger.warning(f"{TerminalColors.YELLOW}{message}{TerminalColors.ENDC}")
|
||||
return True
|
||||
|
||||
for key, expected_values in self.old_and_new_default_contact_values.items():
|
||||
record_field = normalize_string(getattr(record, key))
|
||||
if record_field not in expected_values:
|
||||
message = (
|
||||
f"Skipping '{record}' to avoid potential data corruption. "
|
||||
f"The field '{key}' does not match the default.\n"
|
||||
f"Details: DB value - {record_field}, expected value(s) - {expected_values}"
|
||||
)
|
||||
logger.warning(f"{TerminalColors.YELLOW}{message}{TerminalColors.ENDC}")
|
||||
return True
|
||||
return False
|
|
@ -86,7 +86,9 @@ class PopulateScriptTemplate(ABC):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def mass_update_records(self, object_class, filter_conditions, fields_to_update, debug=True, verbose=False):
|
||||
def mass_update_records(
|
||||
self, object_class, filter_conditions, fields_to_update, debug=True, verbose=False, show_record_count=False
|
||||
):
|
||||
"""Loops through each valid "object_class" object - specified by filter_conditions - and
|
||||
updates fields defined by fields_to_update using update_record.
|
||||
|
||||
|
@ -106,6 +108,9 @@ class PopulateScriptTemplate(ABC):
|
|||
verbose: Whether to print a detailed run summary *before* run confirmation.
|
||||
Default: False.
|
||||
|
||||
show_record_count: Whether to show a 'Record 1/10' dialog when running update.
|
||||
Default: False.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If you do not define update_record before using this function.
|
||||
TypeError: If custom_filter is not Callable.
|
||||
|
@ -115,14 +120,16 @@ class PopulateScriptTemplate(ABC):
|
|||
|
||||
# apply custom filter
|
||||
records = self.custom_filter(records)
|
||||
records_length = len(records)
|
||||
|
||||
readable_class_name = self.get_class_name(object_class)
|
||||
|
||||
# for use in the execution prompt.
|
||||
proposed_changes = f"""==Proposed Changes==
|
||||
Number of {readable_class_name} objects to change: {len(records)}
|
||||
These fields will be updated on each record: {fields_to_update}
|
||||
"""
|
||||
proposed_changes = (
|
||||
"==Proposed Changes==\n"
|
||||
f"Number of {readable_class_name} objects to change: {records_length}\n"
|
||||
f"These fields will be updated on each record: {fields_to_update}"
|
||||
)
|
||||
|
||||
if verbose:
|
||||
proposed_changes = f"""{proposed_changes}
|
||||
|
@ -140,7 +147,9 @@ class PopulateScriptTemplate(ABC):
|
|||
to_update: List[object_class] = []
|
||||
to_skip: List[object_class] = []
|
||||
failed_to_update: List[object_class] = []
|
||||
for record in records:
|
||||
for i, record in enumerate(records, start=1):
|
||||
if show_record_count:
|
||||
logger.info(f"{TerminalColors.BOLD}Record {i}/{records_length}{TerminalColors.ENDC}")
|
||||
try:
|
||||
if not self.should_skip_record(record):
|
||||
self.update_record(record)
|
||||
|
@ -154,7 +163,7 @@ class PopulateScriptTemplate(ABC):
|
|||
logger.error(fail_message)
|
||||
|
||||
# Do a bulk update on the desired field
|
||||
ScriptDataHelper.bulk_update_fields(object_class, to_update, fields_to_update)
|
||||
self.bulk_update_fields(object_class, to_update, fields_to_update)
|
||||
|
||||
# Log what happened
|
||||
TerminalHelper.log_script_run_summary(
|
||||
|
@ -166,6 +175,10 @@ class PopulateScriptTemplate(ABC):
|
|||
display_as_str=True,
|
||||
)
|
||||
|
||||
def bulk_update_fields(self, object_class, to_update, fields_to_update):
|
||||
"""Bulk updates the given fields"""
|
||||
ScriptDataHelper.bulk_update_fields(object_class, to_update, fields_to_update)
|
||||
|
||||
def get_class_name(self, sender) -> str:
|
||||
"""Returns the class name that we want to display for the terminal prompt.
|
||||
Example: DomainRequest => "Domain Request"
|
||||
|
@ -463,4 +476,4 @@ class TerminalHelper:
|
|||
terminal_color = color
|
||||
|
||||
colored_message = f"{terminal_color}{message}{TerminalColors.ENDC}"
|
||||
log_method(colored_message, exc_info=exc_info)
|
||||
return log_method(colored_message, exc_info=exc_info)
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 4.2.17 on 2025-03-17 20:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0143_create_groups_v18"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="eop_stakeholder_email",
|
||||
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name="EOP Stakeholder Email"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="eop_stakeholder_first_name",
|
||||
field=models.CharField(blank=True, null=True, verbose_name="EOP Stakeholder First Name"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="eop_stakeholder_last_name",
|
||||
field=models.CharField(blank=True, null=True, verbose_name="EOP Stakeholder Last Name"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="working_with_eop",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -646,7 +646,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
oldDnssecdata = self.dnssecdata
|
||||
addDnssecdata: dict = {}
|
||||
remDnssecdata: dict = {}
|
||||
|
||||
if _dnssecdata and _dnssecdata.dsData is not None:
|
||||
# initialize addDnssecdata and remDnssecdata for dsData
|
||||
addDnssecdata["dsData"] = _dnssecdata.dsData
|
||||
|
@ -697,15 +696,15 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
added_record = "dsData" in _addDnssecdata and _addDnssecdata["dsData"] is not None
|
||||
deleted_record = "dsData" in _remDnssecdata and _remDnssecdata["dsData"] is not None
|
||||
|
||||
if added_record:
|
||||
registry.send(addRequest, cleaned=True)
|
||||
dsdata_change_log = f"{user_email} added a DS data record"
|
||||
if deleted_record:
|
||||
registry.send(remRequest, cleaned=True)
|
||||
dsdata_change_log = f"{user_email} deleted a DS data record"
|
||||
if added_record:
|
||||
registry.send(addRequest, cleaned=True)
|
||||
if dsdata_change_log != "": # if they add and remove a record at same time
|
||||
dsdata_change_log = f"{user_email} added and deleted a DS data record"
|
||||
else:
|
||||
dsdata_change_log = f"{user_email} deleted a DS data record"
|
||||
dsdata_change_log = f"{user_email} added a DS data record"
|
||||
if dsdata_change_log != "":
|
||||
self.dsdata_last_change = dsdata_change_log
|
||||
self.save() # audit log will now record this as a change
|
||||
|
@ -1703,14 +1702,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32
|
||||
DF = epp.DiscloseField
|
||||
all_disclose_fields = {field for field in DF}
|
||||
disclose_args = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}}
|
||||
disclose_args = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc", DF.NAME: "loc"}}
|
||||
|
||||
fields_to_remove = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT}
|
||||
if contact.contact_type == contact.ContactTypeChoices.SECURITY:
|
||||
if contact.email not in DefaultEmail.get_all_emails():
|
||||
fields_to_remove.add(DF.EMAIL)
|
||||
elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE:
|
||||
fields_to_remove.update({DF.EMAIL, DF.VOICE, DF.ADDR})
|
||||
fields_to_remove.update({DF.NAME, DF.EMAIL, DF.VOICE, DF.ADDR})
|
||||
|
||||
disclose_args["fields"].difference_update(fields_to_remove) # type: ignore
|
||||
|
||||
|
|
|
@ -523,6 +523,29 @@ class DomainRequest(TimeStampedModel):
|
|||
choices=FEBPurposeChoices.choices,
|
||||
)
|
||||
|
||||
working_with_eop = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
eop_stakeholder_first_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="EOP Stakeholder First Name",
|
||||
)
|
||||
|
||||
eop_stakeholder_last_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="EOP Stakeholder Last Name",
|
||||
)
|
||||
|
||||
eop_stakeholder_email = models.EmailField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="EOP Stakeholder Email",
|
||||
)
|
||||
|
||||
# This field is alternately used for generic domain purpose explanations
|
||||
# and for explanations of the specific purpose chosen with feb_purpose_choice
|
||||
# by a Federal Executive Branch agency.
|
||||
|
|
|
@ -164,4 +164,4 @@ class PublicContact(TimeStampedModel):
|
|||
return cls._meta.get_field("registry_id").max_length
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} <{self.email}>" f"id: {self.registry_id} " f"type: {self.contact_type}"
|
||||
return self.registry_id
|
||||
|
|
|
@ -222,3 +222,31 @@ class RestrictAccessMiddleware:
|
|||
raise PermissionDenied # Deny access if the view lacks explicit permission handling
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class RequestLoggingMiddleware:
|
||||
"""
|
||||
Middleware to log user email, remote address, and request path.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
|
||||
# Only log in production (stable)
|
||||
if getattr(settings, "IS_PRODUCTION", False):
|
||||
# Get user email (if authenticated), else "Anonymous"
|
||||
user_email = request.user.email if request.user.is_authenticated else "Anonymous"
|
||||
|
||||
# Get remote IP address
|
||||
remote_ip = request.META.get("REMOTE_ADDR", "Unknown IP")
|
||||
|
||||
# Get request path
|
||||
request_path = request.path
|
||||
|
||||
# Log user information
|
||||
logger.info(f"Router log | User: {user_email} | IP: {remote_ip} | Path: {request_path}")
|
||||
|
||||
return response
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<textarea
|
||||
name="{{ widget.name }}"
|
||||
class="usa-textarea usa-character-count__field {{ widget.attrs.class }}"
|
||||
class="usa-textarea{% if classes %} {{ classes }}{% endif %}{% if not widget.attrs.hide_character_count %} usa-character-count__field{% endif %} {{ widget.attrs.class }}"
|
||||
{% include "django/forms/widgets/attrs.html" %}
|
||||
>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
|
|
@ -9,7 +9,7 @@
|
|||
<div class="grid-container grid-container--widescreen">
|
||||
|
||||
<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 grid-col--sidenav">
|
||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||
text-primary-darker text-semibold string-wrap"
|
||||
>
|
||||
|
@ -21,7 +21,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="tablet:grid-col-9">
|
||||
<div class="tablet:grid-col">
|
||||
<main id="main-content" class="grid-container">
|
||||
{% if not domain.domain_info %}
|
||||
<div class="usa-alert usa-alert--error margin-bottom-2">
|
||||
|
|
|
@ -34,122 +34,376 @@
|
|||
{% endif %}
|
||||
{% endblock breadcrumb %}
|
||||
|
||||
{% if domain.dnssecdata is None %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
You have no DS data added. Enable DNSSEC by adding DS data.
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-6">
|
||||
<h1 class="tablet:margin-bottom-1" id="domain-dsdata">DS data</h1>
|
||||
</div>
|
||||
|
||||
<div class="tablet:grid-col-6 text-right--tablet">
|
||||
<button type="button" class="usa-button margin-bottom-1 tablet:float-right" id="dsdata-add-button">
|
||||
Add DS data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>In order to enable DNSSEC, you must first configure it with your DNS provider.</p>
|
||||
|
||||
<p>Click “Add DS data” and enter the values given by your DNS provider for DS (Delegation Signer) data. You can add a maximum of 8 DS records.</p>
|
||||
|
||||
{% comment %}
|
||||
This template supports the rendering of three different DS data forms, conditionally displayed:
|
||||
1 - Add DS Data form (rendered when there are no existing DS data records defined for the domain)
|
||||
2 - DS Data table (rendered when the domain has existing DS data, which can be viewed and edited)
|
||||
3 - Add DS Data form (rendered above the DS Data table to add a single additional DS Data record)
|
||||
{% endcomment %}
|
||||
|
||||
{% if formset.initial and formset.forms.0.initial %}
|
||||
|
||||
{% comment %}This section renders both the DS Data table and the Add DS Data form {% endcomment %}
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--extra-large ds-data-form" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
{% for form in formset %}
|
||||
{% if forloop.last and not form.initial %}
|
||||
|
||||
{% comment %}
|
||||
This section renders the Add DS data form.
|
||||
This section does not render if the last form has initial data (this occurs if 8 DS data records already exist)
|
||||
{% endcomment %}
|
||||
|
||||
<section class="add-dsdata-form display-none section-outlined section-outlined--extra-padding">
|
||||
<h2 class="margin-top-0">Add DS record</h2>
|
||||
<div class="repeatable-form">
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with sublabel_text="Numbers (0-9) only." %}
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.key_tag %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.algorithm %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest_type %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col">
|
||||
{% with sublabel_text="Numbers (0-9) and letters (a-f) only. SHA-1: 40 chars, SHA-256: 64 chars." %}
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="margin-top-2">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--outline dsdata-cancel-add-form"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS records to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
id="save-ds-data"
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="usa-table-container--scrollable usa-table-container--override-overflow usa-table-container--override-scrollable padding-top-5 margin-top-0" tabindex="0">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked dotgov-table--cell-padding-2-2-2-0" id="dsdata-table">
|
||||
<caption class="sr-only">Your DS data records</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" role="columnheader" class="text-bottom">Key tag</th>
|
||||
<th scope="col" role="columnheader" class="text-bottom">Algorithm</th>
|
||||
<th scope="col" role="columnheader" class="text-bottom">Digest type</th>
|
||||
<th scope="col" role="columnheader" class="text-bottom">Digest</th>
|
||||
<th scope="col" role="columnheader" class="text-bottom width-0 padding-right-0">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in formset %}
|
||||
{% if not forloop.last or form.initial %}
|
||||
|
||||
{% comment %}
|
||||
This section renders table rows for each existing DS data records. Two rows are rendered, a readonly row
|
||||
and an edit row. Only one of which is displayed at a time.
|
||||
{% endcomment %}
|
||||
|
||||
<!-- Readonly row -->
|
||||
<tr class="view-only-row">
|
||||
<td data-label="Key tag">{{ form.key_tag.value }}</td>
|
||||
<td data-label="Algorithm">
|
||||
<span class="ellipsis ellipsis--15">
|
||||
{% for value, label in form.algorithm.field.choices %}
|
||||
{% if value|stringformat:"s" == form.algorithm.value|stringformat:"s" %}
|
||||
{{ label }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Digest type">
|
||||
{% for value, label in form.digest_type.field.choices %}
|
||||
{% if value|stringformat:"s" == form.digest_type.value|stringformat:"s" %}
|
||||
{{ label }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td data-label="Digest">
|
||||
<span class="ellipsis ellipsis--23">{{ form.digest.value }}</span>
|
||||
</td>
|
||||
<td class="padding-right-0" data-label="Action">
|
||||
<div class="tablet:display-flex tablet:flex-row">
|
||||
<button type="button" class='usa-button usa-button--unstyled margin-right-2 margin-top-0 dsdata-edit'>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#edit"></use>
|
||||
</svg>
|
||||
Edit <span class="usa-sr-only">DS record {{forloop.counter}}</span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
role="button"
|
||||
id="button-trigger-delete-dsdata-{{ forloop.counter }}"
|
||||
class="usa-button usa-button--unstyled text-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary visible-mobile-flex dsdata-delete-kebab"
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||
</svg>
|
||||
Delete
|
||||
</a>
|
||||
|
||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
||||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions margin-top-2px"
|
||||
aria-expanded="false"
|
||||
aria-controls="more-actions-dsdata-{{ forloop.counter }}"
|
||||
aria-label="More Actions for DS record {{ forloop.counter }}"
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="more-actions-dsdata-{{ forloop.counter }}" class="usa-accordion__content usa-prose shadow-1 left-auto right-neg-1" hidden>
|
||||
<h2>More options</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled text-underline late-loading-modal-trigger line-height-sans-5 text-secondary dsdata-delete-kebab margin-top-2"
|
||||
name="btn-delete-kebab-click"
|
||||
aria-label="Delete DS record {{ forloop.counter }} from the registry"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Edit row -->
|
||||
<tr class="edit-row display-none">
|
||||
<td class="text-bottom">
|
||||
{% with sublabel_text="(0-65535)." %}
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error margin-top-0" use_small_sublabel_text=True inline_error_class="font-body-xs" %}
|
||||
{% input_with_errors form.key_tag %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="text-bottom">
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error margin-top-0" use_small_sublabel_text=True inline_error_class="font-body-xs" %}
|
||||
{% input_with_errors form.algorithm %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="text-bottom">
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error margin-top-0" use_small_sublabel_text=True inline_error_class="font-body-xs" %}
|
||||
{% input_with_errors form.digest_type %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="text-bottom">
|
||||
{% with sublabel_text="Numbers (0-9) and letters (a-f) only. SHA-1: 40 chars, SHA-256: 64 chars." %}
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error margin-top-0" use_small_sublabel_text=True inline_error_class="font-body-xs" %}
|
||||
{% input_with_errors form.digest %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="padding-right-0 text-bottom" data-label="Action">
|
||||
<button class="usa-button usa-button--unstyled display-block margin-top-1" type="submit">Save</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled display-block dsdata-cancel"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS record form to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled display-block text-secondary dsdata-delete"
|
||||
name="btn-delete-click"
|
||||
aria-label="Delete the DS record from the registry"
|
||||
>Delete
|
||||
</button>
|
||||
<div class="display-none">{{ form.DELETE }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% comment %}
|
||||
This section renders Add DS Data form which renders when there are no existing
|
||||
DS records defined on the domain.
|
||||
{% endcomment %}
|
||||
|
||||
<div class="add-dsdata-form display-none">
|
||||
{% include "includes/required_fields.html" %}
|
||||
<section class="section-outlined section-outlined--extra-padding">
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate>
|
||||
<h2>Add DS record</h2>
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
{% for form in formset %}
|
||||
<div class="repeatable-form">
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with sublabel_text="Numbers (0-9) only." %}
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.key_tag %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.algorithm %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest_type %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col">
|
||||
{% with sublabel_text="Numbers (0-9) and letters (a-f) only. SHA-1: 40 chars, SHA-256: 64 chars." %}
|
||||
{% with hide_character_count=True %}
|
||||
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="margin-top-2">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--outline dsdata-cancel-add-form"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS records to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
id="save-ds-data"
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 id="domain-dsdata">DS data</h1>
|
||||
|
||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||
|
||||
<p>Enter the values given by your DNS provider for DS data.</p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--extra-large ds-data-form" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
{% for form in formset %}
|
||||
<fieldset class="repeatable-form">
|
||||
|
||||
<legend class="sr-only">DS data record {{forloop.counter}}</legend>
|
||||
|
||||
<h2 class="margin-top-0">DS data record {{forloop.counter}}</h2>
|
||||
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.key_tag %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.algorithm %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest_type %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row margin-top-1">
|
||||
<div class="grid-col">
|
||||
<button type="button" id="button label" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record text-secondary line-height-sans-5">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>Delete
|
||||
<span class="sr-only">DS data record {{forloop.counter}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon margin-bottom-2" id="add-form">
|
||||
<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>
|
||||
</svg>Add new record
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="save-ds-data"
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--outline"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS records to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if trigger_modal %}
|
||||
<a
|
||||
id="ds-toggle-dnssec-alert"
|
||||
href="#toggle-dnssec-alert"
|
||||
id="unsaved_changes_trigger"
|
||||
href="#unsaved-changes-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="unsaved-changes-modal"
|
||||
data-open-modal
|
||||
>Trigger unsaved changes modal</a>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="unsaved-changes-modal"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="You have unsaved changes that will be lost."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_id="unsaved-changes-click-button" modal_button_text="Continue without saving" cancel_button_text="Go back" %}
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="cancel_changes_trigger"
|
||||
href="#cancel-changes-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="cancel-changes-modal"
|
||||
data-open-modal
|
||||
>Trigger cancel changes modal</a>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="cancel-changes-modal"
|
||||
aria-labelledby="Are you sure you want to cancel your changes?"
|
||||
aria-describedby="This action cannot be undone."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to cancel your changes?" modal_description="This action cannot be undone." modal_button_id="cancel-changes-click-button" modal_button_text="Yes, cancel" cancel_button_text="Go back" %}
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="delete_trigger"
|
||||
href="#delete-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="delete-modal"
|
||||
data-open-modal
|
||||
>Trigger delete modal</a>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="delete-modal"
|
||||
aria-labelledby="Are you sure you want to delete this DS data record?"
|
||||
aria-describedby="This action cannot be undone."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this DS data record?" modal_description="This action cannot be undone." modal_button_id="delete-click-button" modal_button_text="Yes, delete" modal_button_class="usa-button--secondary" %}
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="disable_dnssec_trigger"
|
||||
href="#disable-dnssec-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="toggle-dnssec-alert"
|
||||
aria-controls="disable-dnssec-modal"
|
||||
data-open-modal
|
||||
>Trigger Disable DNSSEC Modal</a
|
||||
>
|
||||
{% endif %}
|
||||
{# Use data-force-action to take esc out of the equation and pass cancel_button_resets_ds_form to effectuate a reset in the view #}
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-dnssec-alert"
|
||||
id="disable-dnssec-modal"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||
data-force-action
|
||||
>
|
||||
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-override-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-dnssec-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %}
|
||||
</div>
|
||||
<form method="post" id="disable-override-click-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="disable-override-click" value="1">
|
||||
</form>
|
||||
<form method="post" id="btn-cancel-click-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="btn-cancel-click" value="1">
|
||||
</form>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
This section does not render if the last form has initial data (this occurs if 13 nameservers already exist)
|
||||
{% endcomment %}
|
||||
|
||||
<section class="add-nameservers-form display-none section-outlined">
|
||||
<section class="add-nameservers-form display-none section-outlined section-outlined--extra-padding">
|
||||
{{ form.domain }}
|
||||
<h2>Add a name server</h2>
|
||||
<div class="repeatable-form">
|
||||
|
@ -121,7 +121,7 @@
|
|||
|
||||
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked" id="nameserver-table">
|
||||
<caption class="sr-only">Your registered domains</caption>
|
||||
<caption class="sr-only">Your Name server records</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" role="columnheader">Name servers</th>
|
||||
|
@ -141,8 +141,8 @@
|
|||
{{ form.domain }}
|
||||
<!-- Readonly row -->
|
||||
<tr>
|
||||
<td colspan="2" aria-colspan="2">{{ form.server.value }} {% if form.ip.value %}({{ form.ip.value }}){% endif %}</td>
|
||||
<td class="padding-right-0">
|
||||
<td colspan="2" aria-colspan="2" data-label="Name server (IP address)">{{ form.server.value }} {% if form.ip.value %}({{ form.ip.value }}){% endif %}</td>
|
||||
<td class="padding-right-0" data-label="Action">
|
||||
<div class="tablet:display-flex tablet:flex-row">
|
||||
<button type="button" class='usa-button usa-button--unstyled margin-right-2 margin-top-0 nameserver-edit'>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
|
@ -154,7 +154,7 @@
|
|||
<a
|
||||
role="button"
|
||||
id="button-trigger-delete-{{ form.server.value }}"
|
||||
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary visible-mobile-flex nameserver-delete-kebab"
|
||||
class="usa-button usa-button--unstyled text-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary visible-mobile-flex nameserver-delete-kebab"
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||
|
@ -166,12 +166,12 @@
|
|||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions margin-top-0"
|
||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions margin-top-2px"
|
||||
aria-expanded="false"
|
||||
aria-controls="more-actions-{{ form.server.value }}"
|
||||
aria-label="More Actions for ({{ form.server.value }})"
|
||||
>
|
||||
<svg class="usa-icon top-2px" 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="/public/img/sprite.svg#more_vert"></use>
|
||||
</svg>
|
||||
</button>
|
||||
|
@ -180,7 +180,7 @@
|
|||
<h2>More options</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary nameserver-delete-kebab"
|
||||
class="usa-button usa-button--unstyled text-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary nameserver-delete-kebab"
|
||||
name="btn-delete-kebab-click"
|
||||
aria-label="Delete the name server from the registry"
|
||||
>
|
||||
|
@ -206,7 +206,7 @@
|
|||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="padding-right-0 text-bottom">
|
||||
<td class="padding-right-0 text-bottom" data-label="Action">
|
||||
<button class="usa-button usa-button--unstyled display-block margin-top-1" type="submit">Save</button>
|
||||
|
||||
<button
|
||||
|
@ -240,57 +240,58 @@
|
|||
This section renders Add New Nameservers form which renders when there are no existing
|
||||
nameservers defined on the domain.
|
||||
{% endcomment %}
|
||||
|
||||
<section class="add-nameservers-form display-none section-outlined">
|
||||
<div class="add-nameservers-form display-none">
|
||||
{% include "includes/required_fields.html" %}
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate>
|
||||
<h2>Add name servers</h2>
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
{% for form in formset %}
|
||||
{{ form.domain }}
|
||||
<div class="repeatable-form">
|
||||
<div class="grid-row grid-gap-2 flex-end minh-143px">
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% if forloop.counter <= 2 %}
|
||||
{# span_for_text will wrap the copy in s <span>, which we'll use in the JS for this component #}
|
||||
{% with attr_required=True add_initial_value_attr=True span_for_text=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with span_for_text=True add_initial_value_attr=True %}
|
||||
{% input_with_errors form.server %}
|
||||
<section class="section-outlined section-outlined--extra-padding">
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate>
|
||||
<h2>Add name servers</h2>
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
{% for form in formset %}
|
||||
{{ form.domain }}
|
||||
<div class="repeatable-form">
|
||||
<div class="grid-row grid-gap-2 flex-end minh-143px">
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% if forloop.counter <= 2 %}
|
||||
{# span_for_text will wrap the copy in s <span>, which we'll use in the JS for this component #}
|
||||
{% with attr_required=True add_initial_value_attr=True span_for_text=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with attr_required=True add_initial_value_attr=True label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with span_for_text=True add_initial_value_attr=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-6">
|
||||
{% with attr_required=True add_initial_value_attr=True label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="margin-top-2">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--outline nameserver-cancel-add-form"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the name server form to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="margin-top-2">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--outline nameserver-cancel-add-form"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the name server form to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
@ -309,6 +310,22 @@
|
|||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_id="unsaved-changes-click-button" modal_button_text="Continue without saving" cancel_button_text="Go back" %}
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="cancel_changes_trigger"
|
||||
href="#cancel-changes-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="cancel-changes-modal"
|
||||
data-open-modal
|
||||
>Trigger cancel changes modal</a>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="cancel-changes-modal"
|
||||
aria-labelledby="Are you sure you want to cancel your changes?"
|
||||
aria-describedby="This action cannot be undone."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to cancel your changes?" modal_description="This action cannot be undone." modal_button_id="cancel-changes-click-button" modal_button_text="Yes, cancel" cancel_button_text="Go back" %}
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="delete_trigger"
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
{% block content %}
|
||||
<div class="grid-container grid-container--widescreen">
|
||||
<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 grid-col--sidenav">
|
||||
{% include 'domain_request_sidebar.html' %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-9">
|
||||
<div class="tablet:grid-col">
|
||||
<main id="main-content" class="grid-container register-form-step">
|
||||
<input type="hidden" class="display-none" id="wizard-domain-request-id" value="{{domain_request_id}}"/>
|
||||
{% if steps.current == steps.first %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block form_instructions %}
|
||||
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.</p>
|
||||
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
|
||||
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
|
||||
|
||||
|
||||
<h2>What you can’t do with a .gov domain</h2>
|
||||
|
@ -49,23 +49,44 @@
|
|||
|
||||
<h2>Domain renewal</h2>
|
||||
|
||||
<p>.Gov domains are registered for a one-year period. To renew your domain, you'll be asked to verify your organization’s eligibility and your contact information. </p>
|
||||
<p>.Gov domains are registered for a one-year period. To renew the domain, you’ll be asked to verify your contact information and some details about the domain.</p>
|
||||
|
||||
<p>Though a domain may expire, it will not automatically be put on hold or deleted. We’ll make extensive efforts to contact your organization before holding or deleting a domain.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
{% block form_required_fields_help_text %}
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% if requires_feb_questions %}
|
||||
<h2>Required and prohibited activities</h2>
|
||||
<h3>Prohibitions on non-governmental use</h3>
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# commented out so it does not appear on this page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend>
|
||||
<h2>Acknowledgement of .gov domain requirements</h2>
|
||||
</legend>
|
||||
<p>Agencies may not use a .gov domain name:
|
||||
<ul class="usa-list">
|
||||
<li>On behalf of a non-federal executive branch entity</li>
|
||||
<li>For a non-governmental purpose</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Compliance with the 21st Century IDEA Act is required</h3>
|
||||
<p>As required by the DOTGOV Act, agencies must ensure
|
||||
that any website or digital service that uses a .gov
|
||||
domain name is in compliance with the
|
||||
<a href="https://digital.gov/resources/delivering-digital-first-public-experience-act/" target="_blank" rel="noopener noreferrer">21st Century Integrated Digital Experience Act</a>.
|
||||
and
|
||||
<a href="https://bidenwhitehouse.gov/wp-content/uploads/2023/09/M-23-22-Delivering-a-Digital-First-Public-Experience.pdf" target="_blank" rel="noopener noreferrer">Guidance for Agencies</a>.
|
||||
</p>
|
||||
<h2>Acknowledgement of .gov domain requirements</h2>
|
||||
{% input_with_errors forms.0.is_policy_acknowledged %}
|
||||
{% else %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend>
|
||||
<h2>Acknowledgement of .gov domain requirements</h2>
|
||||
</legend>
|
||||
|
||||
</fieldset>
|
||||
{% input_with_errors forms.0.is_policy_acknowledged %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
|
||||
{% with link_href=login_help_url %}
|
||||
{% with sublabel_text="We recommend using a Login.gov account that's only connected to your work email address. If you need to change your email, you'll need to make a change to your Login.gov account. Get help with updating your email address." %}
|
||||
{% with link_text="Get help with updating your email address" target_blank=True do_not_show_max_chars=True %}
|
||||
{% with link_text="Get help with updating your email address" target_blank=True %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
|
|
@ -7,8 +7,10 @@ error messages, if necessary.
|
|||
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% if widget.attrs.maxlength %}
|
||||
<div class="usa-character-count">
|
||||
{% if widget.attrs.maxlength or field.widget_type == 'textarea' %}
|
||||
{% if not widget.attrs.hide_character_count %}
|
||||
<div class="usa-character-count">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if field.use_fieldset %}
|
||||
|
@ -35,7 +37,7 @@ error messages, if necessary.
|
|||
{% endif %}
|
||||
|
||||
{% if sublabel_text %}
|
||||
<p id="{{ widget.attrs.id }}__sublabel" class="text-base margin-top-2px margin-bottom-1">
|
||||
<p id="{{ widget.attrs.id }}__sublabel" class="{% if use_small_sublabel_text %}font-body-xs {% endif %}text-base margin-top-2px margin-bottom-1">
|
||||
{# If the link_text appears more than once, the first instance will be a link and the other instances will be ignored #}
|
||||
{% if link_text and link_text in sublabel_text %}
|
||||
{% with link_index=sublabel_text|find_index:link_text %}
|
||||
|
@ -52,11 +54,11 @@ error messages, if necessary.
|
|||
{% if field.errors %}
|
||||
<div id="{{ widget.attrs.id }}__error-message">
|
||||
{% for error in field.errors %}
|
||||
<div class="usa-error-message display-flex" role="alert">
|
||||
<div class="usa-error-message display-flex{% if inline_error_class %} {{ inline_error_class }}{% endif %}" role="alert">
|
||||
<svg class="usa-icon usa-icon--large" focusable="true" role="img" aria-label="Error">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
|
||||
</svg>
|
||||
<span class="margin-left-05">{{ error }}</span>
|
||||
<span class="margin-left-05 flex-1">{{ error }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -92,14 +94,15 @@ error messages, if necessary.
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if widget.attrs.maxlength and not do_not_show_max_chars %}
|
||||
<span
|
||||
id="{{ widget.attrs.id }}__message"
|
||||
class="usa-character-count__message"
|
||||
aria-live="polite"
|
||||
>
|
||||
You can enter up to {{ widget.attrs.maxlength }} characters
|
||||
</span>
|
||||
|
||||
</div>
|
||||
{% if field.widget_type == 'textarea' or widget.attrs.maxlength %}
|
||||
{% if not widget.attrs.hide_character_count %}
|
||||
<span
|
||||
id="{{ widget.attrs.id }}__message"
|
||||
class="usa-character-count__message"
|
||||
aria-live="polite"
|
||||
>
|
||||
You can enter up to {{ widget.attrs.maxlength }} characters
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -58,18 +58,7 @@
|
|||
{% endif %}
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||
in addition to being a close modal hook {% endcomment %}
|
||||
{% if cancel_button_resets_ds_form %}
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
id="btn-cancel-click-button"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{% elif not cancel_button_only %}
|
||||
{% if not cancel_button_only %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
|
@ -82,30 +71,14 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||
in addition to being a close modal hook {% endcomment %}
|
||||
{% if cancel_button_resets_ds_form %}
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
id="btn-cancel-click-close-button"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
{% else %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
{% with sublabel_text="We recommend using a Login.gov account that's only connected to your work email address. If you need to change your email, you'll need to make a change to your Login.gov account. Get help with updating your email address." %}
|
||||
{% with link_text="Get help with updating your email address" %}
|
||||
{% with target_blank=True %}
|
||||
{% with do_not_show_max_chars=True %}
|
||||
{% with hide_character_count=True %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
|
|
@ -6,16 +6,60 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% if requires_feb_questions %}
|
||||
{{forms.2.management_form}}
|
||||
{{forms.3.management_form}}
|
||||
{{forms.4.management_form}}
|
||||
{{forms.5.management_form}}
|
||||
<fieldset class="usa-fieldset">
|
||||
<h2 class="margin-top-0 margin-bottom-0">Are you working with someone in the Executive Office of the President (EOP) on this request?</h2>
|
||||
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.working_with_eop %}
|
||||
{% endwith %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
<div id="eop-contact-container" class="conditional-panel display-none">
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
Provide the name and email of the person you're working with.<span class="usa-label--required">*</span>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.1.first_name %}
|
||||
{% input_with_errors forms.1.last_name %}
|
||||
{% input_with_errors forms.1.email %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<h2 class="margin-top-0 margin-bottom-0">Is there anything else you'd like us to know about your domain request?</h2>
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.2.has_anything_else_text %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="anything-else-details-container" class="conditional-panel display-none">
|
||||
<p class="usa-label">
|
||||
<em>Provide details below <span class="usa-label--required">*</span></em>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
|
||||
{% input_with_errors forms.3.anything_else %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<h2 class="margin-top-0 margin-bottom-0">Is there anything else you’d like us to know about your domain request?</h2>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
<div id="anything-else">
|
||||
<p><em>This question is optional.</em></p>
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.anything_else %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div id="anything-else">
|
||||
<p><em>This question is optional.</em></p>
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.anything_else %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -114,7 +114,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
|
||||
# do some work for various edge cases
|
||||
|
||||
if "maxlength" in attrs:
|
||||
if "maxlength" in attrs and "hide_character_count" not in attrs:
|
||||
# associate the field programmatically with its hint text
|
||||
described_by.append(f"{attrs['id']}__message")
|
||||
|
||||
|
@ -175,7 +175,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
|
||||
# Conditionally add the data-initial-value attribute
|
||||
if context.get("add_initial_value_attr", False):
|
||||
attrs["data-initial-value"] = field.initial or ""
|
||||
attrs["data-initial-value"] = field.initial if field.initial is not None else ""
|
||||
|
||||
# ask Django to give us the widget dict
|
||||
# see Widget.get_context() on
|
||||
|
|
|
@ -1931,7 +1931,14 @@ class MockEppLib(TestCase):
|
|||
return MagicMock(res_data=[mocked_result])
|
||||
|
||||
def mockCreateContactCommands(self, _request, cleaned):
|
||||
if getattr(_request, "id", None) == "fail" and self.mockedSendFunction.call_count == 3:
|
||||
ids_to_throw_already_exists = [
|
||||
"failAdmin1234567",
|
||||
"failTech12345678",
|
||||
"failSec123456789",
|
||||
"failReg123456789",
|
||||
"fail",
|
||||
]
|
||||
if getattr(_request, "id", None) in ids_to_throw_already_exists and self.mockedSendFunction.call_count == 3:
|
||||
# use this for when a contact is being updated
|
||||
# sets the second send() to fail
|
||||
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
||||
|
@ -1946,7 +1953,14 @@ class MockEppLib(TestCase):
|
|||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
def mockDeleteContactCommands(self, _request, cleaned):
|
||||
if getattr(_request, "id", None) == "fail":
|
||||
ids_to_throw_already_exists = [
|
||||
"failAdmin1234567",
|
||||
"failTech12345678",
|
||||
"failSec123456789",
|
||||
"failReg123456789",
|
||||
"fail",
|
||||
]
|
||||
if getattr(_request, "id", None) in ids_to_throw_already_exists:
|
||||
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
||||
else:
|
||||
return MagicMock(
|
||||
|
@ -1974,7 +1988,7 @@ class MockEppLib(TestCase):
|
|||
disclose_fields = {field for field in DF} - fields
|
||||
|
||||
if disclose_types is None:
|
||||
disclose_types = {DF.ADDR: "loc"}
|
||||
disclose_types = {DF.ADDR: "loc", DF.NAME: "loc"}
|
||||
|
||||
di = common.Disclose(flag=disclose, fields=disclose_fields, types=disclose_types)
|
||||
|
||||
|
|
|
@ -2060,6 +2060,10 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"feb_naming_requirements",
|
||||
"feb_naming_requirements_details",
|
||||
"feb_purpose_choice",
|
||||
"working_with_eop",
|
||||
"eop_stakeholder_first_name",
|
||||
"eop_stakeholder_last_name",
|
||||
"eop_stakeholder_email",
|
||||
"purpose",
|
||||
"has_timeframe",
|
||||
"time_frame_details",
|
||||
|
|
|
@ -32,6 +32,7 @@ from registrar.models import (
|
|||
Portfolio,
|
||||
Suborganization,
|
||||
)
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
import tablib
|
||||
from unittest.mock import patch, call, MagicMock, mock_open
|
||||
from epplibwrapper import commands, common
|
||||
|
@ -2506,3 +2507,189 @@ class TestRemovePortfolios(TestCase):
|
|||
|
||||
# Check that the portfolio was deleted
|
||||
self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists())
|
||||
|
||||
|
||||
class TestUpdateDefaultPublicContacts(MockEppLib):
|
||||
"""Tests for the update_default_public_contacts management command."""
|
||||
|
||||
@less_console_noise_decorator
|
||||
def setUp(self):
|
||||
"""Setup test data with PublicContact records."""
|
||||
super().setUp()
|
||||
self.domain_request = completed_domain_request(
|
||||
name="testdomain.gov",
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
)
|
||||
self.domain_request.approve()
|
||||
self.domain = self.domain_request.approved_domain
|
||||
|
||||
# 1. PublicContact with all old default values
|
||||
self.old_default_contact = self.domain.get_default_administrative_contact()
|
||||
self.old_default_contact.registry_id = "failAdmin1234567"
|
||||
self.old_default_contact.name = "CSD/CB – ATTN: Cameron Dixon"
|
||||
self.old_default_contact.street1 = "CISA – NGR STOP 0645"
|
||||
self.old_default_contact.pc = "20598-0645"
|
||||
self.old_default_contact.email = DefaultEmail.OLD_PUBLIC_CONTACT_DEFAULT
|
||||
self.old_default_contact.save()
|
||||
|
||||
# 2. PublicContact with current default email but old values for other fields
|
||||
self.mixed_default_contact = self.domain.get_default_technical_contact()
|
||||
self.mixed_default_contact.registry_id = "failTech12345678"
|
||||
self.mixed_default_contact.save(skip_epp_save=True)
|
||||
self.mixed_default_contact.name = "registry customer service"
|
||||
self.mixed_default_contact.street1 = "4200 Wilson Blvd."
|
||||
self.mixed_default_contact.pc = "22201"
|
||||
self.mixed_default_contact.email = DefaultEmail.PUBLIC_CONTACT_DEFAULT
|
||||
self.mixed_default_contact.save()
|
||||
|
||||
# 3. PublicContact with non-default values
|
||||
self.non_default_contact = self.domain.get_default_security_contact()
|
||||
self.non_default_contact.registry_id = "failSec123456789"
|
||||
self.non_default_contact.domain = self.domain
|
||||
self.non_default_contact.save(skip_epp_save=True)
|
||||
self.non_default_contact.name = "Hotdogs"
|
||||
self.non_default_contact.street1 = "123 hotdog town"
|
||||
self.non_default_contact.pc = "22111"
|
||||
self.non_default_contact.email = "thehotdogman@igorville.gov"
|
||||
self.non_default_contact.save()
|
||||
|
||||
# 4. Create a default contact but with an old email
|
||||
self.default_registrant_old_email = self.domain.get_default_registrant_contact()
|
||||
self.default_registrant_old_email.registry_id = "failReg123456789"
|
||||
self.default_registrant_old_email.email = DefaultEmail.LEGACY_DEFAULT
|
||||
self.default_registrant_old_email.save()
|
||||
DF = common.DiscloseField
|
||||
excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT}
|
||||
self.all_disclose_fields = {field for field in DF} - excluded_disclose_fields
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test data."""
|
||||
super().tearDown()
|
||||
PublicContact.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True)
|
||||
@less_console_noise_decorator
|
||||
def run_update_default_public_contacts(self, mock_prompt, **kwargs):
|
||||
"""Execute the update_default_public_contacts command with options."""
|
||||
call_command("update_default_public_contacts", **kwargs)
|
||||
|
||||
# @less_console_noise_decorator
|
||||
def test_updates_old_default_contact(self):
|
||||
"""
|
||||
Test that contacts with old default values are updated to new default values.
|
||||
Also tests for string normalization.
|
||||
"""
|
||||
self.run_update_default_public_contacts()
|
||||
self.old_default_contact.refresh_from_db()
|
||||
|
||||
# Verify updates occurred
|
||||
self.assertEqual(self.old_default_contact.name, "CSD/CB – Attn: .gov TLD")
|
||||
self.assertEqual(self.old_default_contact.street1, "1110 N. Glebe Rd")
|
||||
self.assertEqual(self.old_default_contact.pc, "22201")
|
||||
self.assertEqual(self.old_default_contact.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT)
|
||||
|
||||
# Verify EPP create/update calls were made
|
||||
expected_update = self._convertPublicContactToEpp(
|
||||
self.old_default_contact,
|
||||
disclose=False,
|
||||
disclose_fields=self.all_disclose_fields - {"name", "email", "voice", "addr"},
|
||||
)
|
||||
self.mockedSendFunction.assert_any_call(expected_update, cleaned=True)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_updates_with_default_contact_values(self):
|
||||
"""
|
||||
Test that contacts created from the default helper function with old email are updated.
|
||||
"""
|
||||
self.run_update_default_public_contacts()
|
||||
self.default_registrant_old_email.refresh_from_db()
|
||||
|
||||
# Verify updates occurred
|
||||
self.assertEqual(self.default_registrant_old_email.name, "CSD/CB – Attn: .gov TLD")
|
||||
self.assertEqual(self.default_registrant_old_email.street1, "1110 N. Glebe Rd")
|
||||
self.assertEqual(self.default_registrant_old_email.pc, "22201")
|
||||
self.assertEqual(self.default_registrant_old_email.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT)
|
||||
|
||||
# Verify values match the default
|
||||
default_reg = PublicContact.get_default_registrant()
|
||||
self.assertEqual(self.default_registrant_old_email.name, default_reg.name)
|
||||
self.assertEqual(self.default_registrant_old_email.street1, default_reg.street1)
|
||||
self.assertEqual(self.default_registrant_old_email.pc, default_reg.pc)
|
||||
self.assertEqual(self.default_registrant_old_email.email, default_reg.email)
|
||||
|
||||
# Verify EPP create/update calls were made
|
||||
expected_update = self._convertPublicContactToEpp(
|
||||
self.default_registrant_old_email, disclose=False, disclose_fields=self.all_disclose_fields
|
||||
)
|
||||
self.mockedSendFunction.assert_any_call(expected_update, cleaned=True)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_skips_non_default_contacts(self):
|
||||
"""
|
||||
Test that contacts with non-default values are skipped.
|
||||
"""
|
||||
original_name = self.non_default_contact.name
|
||||
original_street1 = self.non_default_contact.street1
|
||||
original_pc = self.non_default_contact.pc
|
||||
original_email = self.non_default_contact.email
|
||||
|
||||
self.run_update_default_public_contacts()
|
||||
self.non_default_contact.refresh_from_db()
|
||||
|
||||
# Verify no updates occurred
|
||||
self.assertEqual(self.non_default_contact.name, original_name)
|
||||
self.assertEqual(self.non_default_contact.street1, original_street1)
|
||||
self.assertEqual(self.non_default_contact.pc, original_pc)
|
||||
self.assertEqual(self.non_default_contact.email, original_email)
|
||||
|
||||
# Ensure that the update is still skipped even with the override flag
|
||||
self.run_update_default_public_contacts(overwrite_updated_contacts=True)
|
||||
self.non_default_contact.refresh_from_db()
|
||||
|
||||
# Verify no updates occurred
|
||||
self.assertEqual(self.non_default_contact.name, original_name)
|
||||
self.assertEqual(self.non_default_contact.street1, original_street1)
|
||||
self.assertEqual(self.non_default_contact.pc, original_pc)
|
||||
self.assertEqual(self.non_default_contact.email, original_email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_skips_contacts_with_current_default_email_by_default(self):
|
||||
"""
|
||||
Test that contacts with the current default email are skipped when not using the override flag.
|
||||
"""
|
||||
# Get original values
|
||||
original_name = self.mixed_default_contact.name
|
||||
original_street1 = self.mixed_default_contact.street1
|
||||
|
||||
self.run_update_default_public_contacts()
|
||||
self.mixed_default_contact.refresh_from_db()
|
||||
|
||||
# Verify no updates occurred
|
||||
self.assertEqual(self.mixed_default_contact.name, original_name)
|
||||
self.assertEqual(self.mixed_default_contact.street1, original_street1)
|
||||
self.assertEqual(self.mixed_default_contact.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_updates_with_overwrite_flag(self):
|
||||
"""
|
||||
Test that contacts with the current default email are updated when using the override flag.
|
||||
"""
|
||||
# Run the command with the override flag
|
||||
self.run_update_default_public_contacts(overwrite_updated_contacts=True)
|
||||
self.mixed_default_contact.refresh_from_db()
|
||||
|
||||
# Verify updates occurred
|
||||
self.assertEqual(self.mixed_default_contact.name, "CSD/CB – Attn: .gov TLD")
|
||||
self.assertEqual(self.mixed_default_contact.street1, "1110 N. Glebe Rd")
|
||||
self.assertEqual(self.mixed_default_contact.pc, "22201")
|
||||
self.assertEqual(self.mixed_default_contact.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT)
|
||||
|
||||
# Verify EPP create/update calls were made
|
||||
expected_update = self._convertPublicContactToEpp(
|
||||
self.mixed_default_contact, disclose=False, disclose_fields=self.all_disclose_fields
|
||||
)
|
||||
self.mockedSendFunction.assert_any_call(expected_update, cleaned=True)
|
||||
|
|
48
src/registrar/tests/test_middleware_logging.py
Normal file
48
src/registrar/tests/test_middleware_logging.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from django.test import TestCase, RequestFactory, override_settings
|
||||
from unittest.mock import patch, MagicMock
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
|
||||
from registrar.registrar_middleware import RequestLoggingMiddleware
|
||||
|
||||
|
||||
class RequestLoggingMiddlewareTest(TestCase):
|
||||
"""Test 'our' middleware logging."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.get_response_mock = MagicMock()
|
||||
self.middleware = RequestLoggingMiddleware(self.get_response_mock)
|
||||
|
||||
@override_settings(IS_PRODUCTION=True) # Scopes change to this test only
|
||||
@patch("logging.Logger.info")
|
||||
def test_logging_enabled_in_production(self, mock_logger):
|
||||
"""Test that logging occurs when IS_PRODUCTION is True"""
|
||||
request = self.factory.get("/test-path", **{"REMOTE_ADDR": "Unknown IP"}) # Override IP
|
||||
request.user = User(username="testuser", email="testuser@example.com")
|
||||
|
||||
self.middleware(request) # Call middleware
|
||||
|
||||
mock_logger.assert_called_once_with(
|
||||
"Router log | User: testuser@example.com | IP: Unknown IP | Path: /test-path"
|
||||
)
|
||||
|
||||
@patch("logging.Logger.info")
|
||||
def test_logging_disabled_in_non_production(self, mock_logger):
|
||||
"""Test that logging does not occur when IS_PRODUCTION is False"""
|
||||
request = self.factory.get("/test-path")
|
||||
request.user = User(username="testuser", email="testuser@example.com")
|
||||
|
||||
self.middleware(request) # Call middleware
|
||||
|
||||
mock_logger.assert_not_called() # Ensure no logs are generated
|
||||
|
||||
@override_settings(IS_PRODUCTION=True) # Scopes change to this test only
|
||||
@patch("logging.Logger.info")
|
||||
def test_logging_anonymous_user(self, mock_logger):
|
||||
"""Test logging for an anonymous user"""
|
||||
request = self.factory.get("/anonymous-path", **{"REMOTE_ADDR": "Unknown IP"}) # Override IP
|
||||
request.user = AnonymousUser() # Simulate an anonymous user
|
||||
|
||||
self.middleware(request) # Call middleware
|
||||
|
||||
mock_logger.assert_called_once_with("Router log | User: Anonymous | IP: Unknown IP | Path: /anonymous-path")
|
|
@ -1003,12 +1003,12 @@ class TestRegistrantContacts(MockEppLib):
|
|||
expected_contact, disclose=False, disclose_fields=disclose_fields
|
||||
)
|
||||
elif expected_contact.contact_type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
||||
disclose_fields = self.all_disclose_fields - {"email", "voice", "addr"}
|
||||
disclose_fields = self.all_disclose_fields - {"name", "email", "voice", "addr"}
|
||||
expectedCreateCommand = self._convertPublicContactToEpp(
|
||||
expected_contact,
|
||||
disclose=False,
|
||||
disclose_fields=disclose_fields,
|
||||
disclose_types={"addr": "loc"},
|
||||
disclose_types={"addr": "loc", "name": "loc"},
|
||||
)
|
||||
else:
|
||||
expectedCreateCommand = self._convertPublicContactToEpp(
|
||||
|
@ -1029,7 +1029,9 @@ class TestRegistrantContacts(MockEppLib):
|
|||
DF = common.DiscloseField
|
||||
expected_disclose = {
|
||||
"auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
"disclose": common.Disclose(flag=False, fields=disclose_email_field, types={DF.ADDR: "loc"}),
|
||||
"disclose": common.Disclose(
|
||||
flag=False, fields=disclose_email_field, types={DF.ADDR: "loc", DF.NAME: "loc"}
|
||||
),
|
||||
"email": "help@get.gov",
|
||||
"extensions": [],
|
||||
"fax": None,
|
||||
|
@ -1054,7 +1056,9 @@ class TestRegistrantContacts(MockEppLib):
|
|||
# Separated for linter
|
||||
expected_not_disclose = {
|
||||
"auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
"disclose": common.Disclose(flag=False, fields=disclose_email_field, types={DF.ADDR: "loc"}),
|
||||
"disclose": common.Disclose(
|
||||
flag=False, fields=disclose_email_field, types={DF.ADDR: "loc", DF.NAME: "loc"}
|
||||
),
|
||||
"email": "help@get.gov",
|
||||
"extensions": [],
|
||||
"fax": None,
|
||||
|
@ -1113,7 +1117,7 @@ class TestRegistrantContacts(MockEppLib):
|
|||
# Verify disclosure settings
|
||||
self.assertEqual(result.disclose.flag, True)
|
||||
self.assertEqual(result.disclose.fields, {DF.EMAIL})
|
||||
self.assertEqual(result.disclose.types, {DF.ADDR: "loc"})
|
||||
self.assertEqual(result.disclose.types, {DF.ADDR: "loc", DF.NAME: "loc"})
|
||||
|
||||
def test_not_disclosed_on_default_security_contact(self):
|
||||
"""
|
||||
|
|
|
@ -2547,8 +2547,8 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
domain DNSSEC data and shows a button to Add new record"""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dnssec_none.id}))
|
||||
self.assertContains(page, "You have no DS data added")
|
||||
self.assertContains(page, "Add new record")
|
||||
self.assertEqual(page.status_code, 200)
|
||||
self.assertContains(page, "Add DS record")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_form_loads_with_ds_data(self):
|
||||
|
@ -2556,26 +2556,8 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
domain DNSSEC DS data and shows the data"""
|
||||
|
||||
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
self.assertContains(page, "DS data record 1")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_data_form_modal(self):
|
||||
"""When user clicks on save, a modal pops up."""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
# Assert that a hidden trigger for the modal does not exist.
|
||||
# This hidden trigger will pop on the page when certain condition are met:
|
||||
# 1) Initial form contained DS data, 2) All data is deleted and form is
|
||||
# submitted.
|
||||
self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal")
|
||||
# Simulate a delete all data
|
||||
form_data = {}
|
||||
response = self.client.post(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}),
|
||||
data=form_data,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200) # Adjust status code as needed
|
||||
# Now check to see whether the JS trigger for the modal is present on the page
|
||||
self.assertContains(response, "Trigger Disable DNSSEC Modal")
|
||||
self.assertContains(page, "Add DS record") # assert add form is present
|
||||
self.assertContains(page, "Action") # assert table is present
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_data_form_submits(self):
|
||||
|
@ -2620,6 +2602,32 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
self.assertContains(result, "Digest type is required", count=2, status_code=200)
|
||||
self.assertContains(result, "Digest is required", count=2, status_code=200)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_data_form_duplicate(self):
|
||||
"""DS data form errors with invalid data (duplicate DS)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# all four form fields are required, so will test with each blank
|
||||
add_data_page.forms[0]["form-0-key_tag"] = 1234
|
||||
add_data_page.forms[0]["form-0-algorithm"] = 3
|
||||
add_data_page.forms[0]["form-0-digest_type"] = 1
|
||||
add_data_page.forms[0]["form-0-digest"] = "ec0bdd990b39feead889f0ba613db4adec0bdd99"
|
||||
add_data_page.forms[0]["form-1-key_tag"] = 1234
|
||||
add_data_page.forms[0]["form-1-algorithm"] = 3
|
||||
add_data_page.forms[0]["form-1-digest_type"] = 1
|
||||
add_data_page.forms[0]["form-1-digest"] = "ec0bdd990b39feead889f0ba613db4adec0bdd99"
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# the field.
|
||||
self.assertContains(
|
||||
result, "You already entered this DS record. DS records must be unique.", count=2, status_code=200
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_data_form_invalid_keytag(self):
|
||||
"""DS data form errors with invalid data (key tag too large)
|
||||
|
@ -2643,6 +2651,29 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_data_form_invalid_keytag_chars(self):
|
||||
"""DS data form errors with invalid data (key tag not numeric)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
# get a form error
|
||||
add_data_page.forms[0]["form-0-key_tag"] = "invalid" # not numeric
|
||||
add_data_page.forms[0]["form-0-algorithm"] = ""
|
||||
add_data_page.forms[0]["form-0-digest_type"] = ""
|
||||
add_data_page.forms[0]["form-0-digest"] = ""
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# the field.
|
||||
self.assertContains(
|
||||
result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_CHARS)), count=2, status_code=200
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_ds_data_form_invalid_digest_chars(self):
|
||||
"""DS data form errors with invalid data (digest contains non hexadecimal chars)
|
||||
|
@ -2698,8 +2729,6 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"domain_pk": self.domain_dsdata.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
# get a form error
|
||||
add_data_page.forms[0]["form-0-key_tag"] = "1234"
|
||||
add_data_page.forms[0]["form-0-algorithm"] = "3"
|
||||
add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256
|
||||
|
|
|
@ -2550,7 +2550,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
# @less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_domain_request_dotgov_domain_FEB_questions(self):
|
||||
def test_domain_request_FEB_questions(self):
|
||||
"""
|
||||
Test that for a member of a federal executive branch portfolio with org feature on, the dotgov domain page
|
||||
contains additional questions for OMB.
|
||||
|
@ -2612,7 +2612,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# separate out these tests for readability
|
||||
self.feb_dotgov_domain_tests(dotgov_page)
|
||||
|
||||
# Now proceed with the actual test
|
||||
domain_form = dotgov_page.forms[0]
|
||||
domain = "test.gov"
|
||||
domain_form["dotgov_domain-requested_domain"] = domain
|
||||
|
@ -2630,6 +2629,36 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
self.feb_purpose_page_tests(purpose_page)
|
||||
|
||||
purpose_form = purpose_page.forms[0]
|
||||
purpose_form["purpose-feb_purpose_choice"] = "redirect"
|
||||
purpose_form["purpose-purpose"] = "test"
|
||||
purpose_form["purpose-has_timeframe"] = "True"
|
||||
purpose_form["purpose-time_frame_details"] = "test"
|
||||
purpose_form["purpose-is_interagency_initiative"] = "True"
|
||||
purpose_form["purpose-interagency_initiative_details"] = "test"
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
purpose_result = purpose_form.submit()
|
||||
|
||||
# ---- ADDITIONAL DETAILS PAGE ----
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
additional_details_page = purpose_result.follow()
|
||||
self.feb_additional_details_page_tests(additional_details_page)
|
||||
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
additional_details_form["portfolio_additional_details-working_with_eop"] = "True"
|
||||
additional_details_form["portfolio_additional_details-first_name"] = "Testy"
|
||||
additional_details_form["portfolio_additional_details-last_name"] = "Tester"
|
||||
additional_details_form["portfolio_additional_details-email"] = "testy@town.com"
|
||||
additional_details_form["portfolio_additional_details-has_anything_else_text"] = "True"
|
||||
additional_details_form["portfolio_additional_details-anything_else"] = "test"
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
additional_details_result = additional_details_form.submit()
|
||||
|
||||
# ---- REQUIREMENTS PAGE ----
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
requirements_page = additional_details_result.follow()
|
||||
self.feb_requirements_page_tests(requirements_page)
|
||||
|
||||
def feb_purpose_page_tests(self, purpose_page):
|
||||
self.assertContains(purpose_page, "What is the purpose of your requested domain?")
|
||||
|
||||
|
@ -2670,6 +2699,40 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# Check that the details form was included
|
||||
self.assertContains(dotgov_page, "feb_naming_requirements_details")
|
||||
|
||||
def feb_additional_details_page_tests(self, additional_details_page):
|
||||
test_text = "Are you working with someone in the Executive Office of the President (EOP) on this request?"
|
||||
self.assertContains(additional_details_page, test_text)
|
||||
|
||||
# Make sure the EOP form is present
|
||||
self.assertContains(additional_details_page, "working_with_eop")
|
||||
|
||||
# Make sure the EOP contact form is present
|
||||
self.assertContains(additional_details_page, "eop-contact-container")
|
||||
self.assertContains(additional_details_page, "additional_details-first_name")
|
||||
self.assertContains(additional_details_page, "additional_details-last_name")
|
||||
self.assertContains(additional_details_page, "additional_details-email")
|
||||
|
||||
# Make sure the additional details form is present
|
||||
self.assertContains(additional_details_page, "additional_details-has_anything_else_text")
|
||||
self.assertContains(additional_details_page, "additional_details-anything_else")
|
||||
|
||||
def feb_requirements_page_tests(self, requirements_page):
|
||||
# Check for the 21st Century IDEA Act links
|
||||
self.assertContains(
|
||||
requirements_page, "https://digital.gov/resources/delivering-digital-first-public-experience-act/"
|
||||
)
|
||||
self.assertContains(
|
||||
requirements_page,
|
||||
"https://bidenwhitehouse.gov/wp-content/uploads/2023/09/M-23-22-Delivering-a-Digital-First-Public-Experience.pdf", # noqa
|
||||
)
|
||||
|
||||
# Check for the policy acknowledgement form
|
||||
self.assertContains(requirements_page, "is_policy_acknowledged")
|
||||
self.assertContains(
|
||||
requirements_page,
|
||||
"I read and understand the guidance outlined in the DOTGOV Act for operating a .gov domain.",
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_formsets(self):
|
||||
"""Users are able to add more than one of some fields."""
|
||||
|
|
|
@ -238,6 +238,7 @@ class DsDataErrorCodes(IntEnum):
|
|||
- 3 INVALID_DIGEST_SHA256 invalid digest for digest type SHA-256
|
||||
- 4 INVALID_DIGEST_CHARS invalid chars in digest
|
||||
- 5 INVALID_KEYTAG_SIZE invalid key tag size > 65535
|
||||
- 6 INVALID_KEYTAG_CHARS invalid key tag, not numeric
|
||||
"""
|
||||
|
||||
BAD_DATA = 1
|
||||
|
@ -245,6 +246,7 @@ class DsDataErrorCodes(IntEnum):
|
|||
INVALID_DIGEST_SHA256 = 3
|
||||
INVALID_DIGEST_CHARS = 4
|
||||
INVALID_KEYTAG_SIZE = 5
|
||||
INVALID_KEYTAG_CHARS = 6
|
||||
|
||||
|
||||
class DsDataError(Exception):
|
||||
|
@ -260,7 +262,8 @@ class DsDataError(Exception):
|
|||
DsDataErrorCodes.INVALID_DIGEST_SHA1: ("SHA-1 digest must be exactly 40 characters."),
|
||||
DsDataErrorCodes.INVALID_DIGEST_SHA256: ("SHA-256 digest must be exactly 64 characters."),
|
||||
DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters (0-9, a-f)."),
|
||||
DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535."),
|
||||
DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Enter a number between 0 and 65535."),
|
||||
DsDataErrorCodes.INVALID_KEYTAG_CHARS: ("Key tag must be numeric (0-9)."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
|
|
|
@ -1064,10 +1064,6 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
for record in dnssecdata.dsData
|
||||
)
|
||||
|
||||
# Ensure at least 1 record, filled or empty
|
||||
while len(initial_data) == 0:
|
||||
initial_data.append({})
|
||||
|
||||
return initial_data
|
||||
|
||||
def get_success_url(self):
|
||||
|
@ -1086,29 +1082,8 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
"""Formset submission posts to this view."""
|
||||
self._get_domain(request)
|
||||
formset = self.get_form()
|
||||
override = False
|
||||
|
||||
# This is called by the form cancel button,
|
||||
# and also by the modal's X and cancel buttons
|
||||
if "btn-cancel-click" in request.POST:
|
||||
url = self.get_success_url()
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# This is called by the Disable DNSSEC modal to override
|
||||
if "disable-override-click" in request.POST:
|
||||
override = True
|
||||
|
||||
# This is called when all DNSSEC data has been deleted and the
|
||||
# Save button is pressed
|
||||
if len(formset) == 0 and formset.initial != [{}] and override is False:
|
||||
# trigger the modal
|
||||
# get context data from super() rather than self
|
||||
# to preserve the context["form"]
|
||||
context = super().get_context_data(form=formset)
|
||||
context["trigger_modal"] = True
|
||||
return self.render_to_response(context)
|
||||
|
||||
if formset.is_valid() or override:
|
||||
if formset.is_valid():
|
||||
return self.form_valid(formset)
|
||||
else:
|
||||
return self.form_invalid(formset)
|
||||
|
@ -1120,11 +1095,12 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
dnssecdata = extensions.DNSSECExtension()
|
||||
|
||||
for form in formset:
|
||||
if form.cleaned_data.get("DELETE"): # Check if form is marked for deletion
|
||||
continue # Skip processing this form
|
||||
|
||||
try:
|
||||
# if 'delete' not in form.cleaned_data
|
||||
# or form.cleaned_data['delete'] == False:
|
||||
dsrecord = {
|
||||
"keyTag": form.cleaned_data["key_tag"],
|
||||
"keyTag": int(form.cleaned_data["key_tag"]),
|
||||
"alg": int(form.cleaned_data["algorithm"]),
|
||||
"digestType": int(form.cleaned_data["digest_type"]),
|
||||
"digest": form.cleaned_data["digest"],
|
||||
|
|
|
@ -603,7 +603,49 @@ class RequestingEntity(DomainRequestWizard):
|
|||
class PortfolioAdditionalDetails(DomainRequestWizard):
|
||||
template_name = "portfolio_domain_request_additional_details.html"
|
||||
|
||||
forms = [forms.PortfolioAnythingElseForm]
|
||||
forms = [
|
||||
feb.WorkingWithEOPYesNoForm,
|
||||
feb.EOPContactForm,
|
||||
feb.FEBAnythingElseYesNoForm,
|
||||
forms.PortfolioAnythingElseForm,
|
||||
]
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["requires_feb_questions"] = self.requires_feb_questions()
|
||||
return context
|
||||
|
||||
def is_valid(self, forms: list) -> bool:
|
||||
"""
|
||||
Validates the forms for portfolio additional details.
|
||||
|
||||
Expected order of forms_list:
|
||||
0: WorkingWithEOPYesNoForm
|
||||
1: EOPContactForm
|
||||
2: FEBAnythingElseYesNoForm
|
||||
3: PortfolioAnythingElseForm
|
||||
"""
|
||||
eop_forms_valid = True
|
||||
if not forms[0].is_valid():
|
||||
# If the user isn't working with EOP, don't validate the EOP contact form
|
||||
forms[1].mark_form_for_deletion()
|
||||
eop_forms_valid = False
|
||||
if forms[0].cleaned_data.get("working_with_eop"):
|
||||
eop_forms_valid = forms[1].is_valid()
|
||||
else:
|
||||
forms[1].mark_form_for_deletion()
|
||||
anything_else_forms_valid = True
|
||||
if not forms[2].is_valid():
|
||||
forms[3].mark_form_for_deletion()
|
||||
anything_else_forms_valid = False
|
||||
if forms[2].cleaned_data.get("has_anything_else_text"):
|
||||
forms[3].fields["anything_else"].required = True
|
||||
forms[3].fields["anything_else"].error_messages[
|
||||
"required"
|
||||
] = "Please provide additional details you'd like us to know. \
|
||||
If you have nothing to add, select 'No'."
|
||||
anything_else_forms_valid = forms[3].is_valid()
|
||||
return eop_forms_valid and anything_else_forms_valid
|
||||
|
||||
|
||||
# Non-portfolio pages
|
||||
|
@ -887,6 +929,29 @@ class Requirements(DomainRequestWizard):
|
|||
template_name = "domain_request_requirements.html"
|
||||
forms = [forms.RequirementsForm]
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["requires_feb_questions"] = self.requires_feb_questions()
|
||||
return context
|
||||
|
||||
# Override the get_forms method to set the policy acknowledgement label conditionally based on feb status
|
||||
def get_forms(self, step=None, use_post=False, use_db=False, files=None):
|
||||
forms_list = super().get_forms(step, use_post, use_db, files)
|
||||
|
||||
# Pass the is_federal context to the form
|
||||
for form in forms_list:
|
||||
if isinstance(form, forms.RequirementsForm):
|
||||
if self.requires_feb_questions():
|
||||
form.fields["is_policy_acknowledged"].label = (
|
||||
"I read and understand the guidance outlined in the DOTGOV Act for operating a .gov domain." # noqa: E501
|
||||
)
|
||||
else:
|
||||
form.fields["is_policy_acknowledged"].label = (
|
||||
"I read and agree to the requirements for operating a .gov domain." # noqa: E501
|
||||
)
|
||||
|
||||
return forms_list
|
||||
|
||||
|
||||
class Review(DomainRequestWizard):
|
||||
template_name = "domain_request_review.html"
|
||||
|
@ -899,6 +964,7 @@ class Review(DomainRequestWizard):
|
|||
context = super().get_context_data()
|
||||
context["Step"] = self.get_step_enum().__members__
|
||||
context["domain_request"] = self.domain_request
|
||||
context["requires_feb_questions"] = self.requires_feb_questions()
|
||||
return context
|
||||
|
||||
def goto_next_step(self):
|
||||
|
|
|
@ -1,68 +1,69 @@
|
|||
-i https://pypi.python.org/simple
|
||||
annotated-types==0.7.0; python_version >= '3.8'
|
||||
asgiref==3.8.1; python_version >= '3.8'
|
||||
boto3==1.35.91; python_version >= '3.8'
|
||||
botocore==1.35.91; python_version >= '3.8'
|
||||
cachetools==5.5.0; python_version >= '3.7'
|
||||
certifi==2024.12.14; python_version >= '3.6'
|
||||
boto3==1.37.18; python_version >= '3.8'
|
||||
botocore==1.37.18; python_version >= '3.8'
|
||||
cachetools==5.5.2; python_version >= '3.7'
|
||||
certifi==2025.1.31; python_version >= '3.6'
|
||||
cfenv==0.5.3
|
||||
cffi==1.17.1; python_version >= '3.8'
|
||||
charset-normalizer==3.4.1; python_version >= '3.7'
|
||||
cryptography==44.0.0; python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'
|
||||
cryptography==44.0.2; python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'
|
||||
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
diff-match-patch==20241021; python_version >= '3.7'
|
||||
dj-database-url==2.3.0
|
||||
dj-email-url==1.0.6
|
||||
django==4.2.17; python_version >= '3.8'
|
||||
django==4.2.20; python_version >= '3.8'
|
||||
django-admin-multiple-choice-list-filter==0.1.1
|
||||
django-allow-cidr==0.7.1
|
||||
django-auditlog==3.0.0; python_version >= '3.8'
|
||||
django-cache-url==3.4.5
|
||||
django-cors-headers==4.6.0; python_version >= '3.9'
|
||||
django-cors-headers==4.7.0; python_version >= '3.9'
|
||||
django-csp==3.8
|
||||
django-fsm==2.8.1
|
||||
django-import-export==4.3.3; python_version >= '3.9'
|
||||
django-import-export==4.3.7; python_version >= '3.9'
|
||||
django-login-required-middleware==0.9.0
|
||||
django-phonenumber-field[phonenumberslite]==8.0.0; python_version >= '3.8'
|
||||
django-waffle==4.2.0; python_version >= '3.8'
|
||||
django-widget-tweaks==1.5.0; python_version >= '3.8'
|
||||
environs[django]==11.2.1; python_version >= '3.8'
|
||||
faker==33.1.0; python_version >= '3.8'
|
||||
fred-epplib @ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
|
||||
furl==2.1.3
|
||||
environs[django]==14.1.1; python_version >= '3.9'
|
||||
faker==37.0.2; python_version >= '3.9'
|
||||
fred-epplib @ git+https://github.com/cisagov/epplib.git@9f0fd0e69665001767f15a034c9c0c919dab5cdd
|
||||
furl==2.1.4
|
||||
future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
|
||||
gevent==24.11.1; python_version >= '3.9'
|
||||
greenlet==3.1.1; python_version >= '3.7'
|
||||
gunicorn==23.0.0; python_version >= '3.7'
|
||||
idna==3.10; python_version >= '3.6'
|
||||
jmespath==1.0.1; python_version >= '3.7'
|
||||
lxml==5.3.0; python_version >= '3.6'
|
||||
mako==1.3.8; python_version >= '3.8'
|
||||
lxml==5.3.1; python_version >= '3.6'
|
||||
mako==1.3.9; python_version >= '3.8'
|
||||
markupsafe==3.0.2; python_version >= '3.9'
|
||||
marshmallow==3.23.2; python_version >= '3.9'
|
||||
marshmallow==3.26.1; python_version >= '3.9'
|
||||
oic==1.7.0; python_version ~= '3.8'
|
||||
orderedmultidict==1.0.1
|
||||
packaging==24.2; python_version >= '3.8'
|
||||
phonenumberslite==8.13.52
|
||||
phonenumberslite==9.0.1
|
||||
psycopg2-binary==2.9.10; python_version >= '3.8'
|
||||
pycparser==2.22; python_version >= '3.8'
|
||||
pycryptodomex==3.21.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
pydantic==2.10.4; python_version >= '3.8'
|
||||
pycryptodomex==3.22.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
|
||||
pydantic==2.10.6; python_version >= '3.8'
|
||||
pydantic-core==2.27.2; python_version >= '3.8'
|
||||
pydantic-settings==2.7.1; python_version >= '3.8'
|
||||
pydantic-settings==2.8.1; python_version >= '3.8'
|
||||
pyjwkest==1.4.2
|
||||
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
|
||||
python-dotenv==1.0.1; python_version >= '3.8'
|
||||
pyzipper==0.3.6; python_version >= '3.4'
|
||||
requests==2.32.3; python_version >= '3.8'
|
||||
s3transfer==0.10.4; python_version >= '3.8'
|
||||
setuptools==75.6.0; python_version >= '3.9'
|
||||
s3transfer==0.11.4; python_version >= '3.8'
|
||||
setuptools==77.0.3; python_version >= '3.9'
|
||||
six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
|
||||
sqlparse==0.5.3; python_version >= '3.8'
|
||||
tablib==3.7.0; python_version >= '3.9'
|
||||
tablib==3.8.0; python_version >= '3.9'
|
||||
tblib==3.0.0; python_version >= '3.8'
|
||||
typing-extensions==4.12.2; python_version >= '3.8'
|
||||
tzdata==2025.1; python_version >= '2'
|
||||
urllib3==2.3.0; python_version >= '3.9'
|
||||
whitenoise==6.8.2; python_version >= '3.9'
|
||||
whitenoise==6.9.0; python_version >= '3.9'
|
||||
zope.event==5.0; python_version >= '3.7'
|
||||
zope.interface==7.2; python_version >= '3.8'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue