mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-03 09:43:33 +02:00
Merge branch 'main' into rjm/doc-uswds-updates
This commit is contained in:
commit
eec4cd3b7a
23 changed files with 1195 additions and 313 deletions
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainDNSSEC() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainDSData() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainManagersPage() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { submitForm } from './helpers.js';
|
||||
import { submitForm } from './form-helpers.js';
|
||||
|
||||
export function initDomainRequestForm() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
57
src/registrar/assets/src/js/getgov/form-helpers.js
Normal file
57
src/registrar/assets/src/js/getgov/form-helpers.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Helper function to submit a form
|
||||
* @param {} form_id - the id of the form to be submitted
|
||||
*/
|
||||
export function submitForm(form_id) {
|
||||
let form = document.getElementById(form_id);
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else {
|
||||
console.error("Form '" + form_id + "' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all error-related classes and messages from the specified DOM element.
|
||||
* This method cleans up validation errors by removing error highlighting from input fields,
|
||||
* labels, and form groups, as well as deleting error message elements.
|
||||
* @param {HTMLElement} domElement - The parent element within which errors should be cleared.
|
||||
*/
|
||||
export function removeErrorsFromElement(domElement) {
|
||||
// Remove the 'usa-form-group--error' class from all div elements
|
||||
domElement.querySelectorAll("div.usa-form-group--error").forEach(div => {
|
||||
div.classList.remove("usa-form-group--error");
|
||||
});
|
||||
|
||||
// Remove the 'usa-label--error' class from all label elements
|
||||
domElement.querySelectorAll("label.usa-label--error").forEach(label => {
|
||||
label.classList.remove("usa-label--error");
|
||||
});
|
||||
|
||||
// Remove all error message divs whose ID ends with '__error-message'
|
||||
domElement.querySelectorAll("div[id$='__error-message']").forEach(errorDiv => {
|
||||
errorDiv.remove();
|
||||
});
|
||||
|
||||
// Remove the 'usa-input--error' class from all input elements
|
||||
domElement.querySelectorAll("input.usa-input--error").forEach(input => {
|
||||
input.classList.remove("usa-input--error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all form-level error messages displayed in the UI.
|
||||
* The form error messages are contained within div elements with the ID 'form-errors'.
|
||||
* Since multiple elements with the same ID may exist (even though not syntactically correct),
|
||||
* this function removes them iteratively.
|
||||
*/
|
||||
export function removeFormErrors() {
|
||||
let formErrorDiv = document.getElementById("form-errors");
|
||||
|
||||
// Recursively remove all instances of form error divs
|
||||
while (formErrorDiv) {
|
||||
formErrorDiv.remove();
|
||||
formErrorDiv = document.getElementById("form-errors");
|
||||
}
|
||||
}
|
516
src/registrar/assets/src/js/getgov/form-nameservers.js
Normal file
516
src/registrar/assets/src/js/getgov/form-nameservers.js
Normal file
|
@ -0,0 +1,516 @@
|
|||
import { showElement, hideElement, scrollToElement } from './helpers';
|
||||
import { removeErrorsFromElement, removeFormErrors } from './form-helpers';
|
||||
|
||||
export class NameserverForm {
|
||||
constructor() {
|
||||
this.addNameserverButton = document.getElementById('nameserver-add-button');
|
||||
this.addNameserversForm = document.querySelector('.add-nameservers-form');
|
||||
this.domain = '';
|
||||
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 NameserverForm by setting up display and event listeners.
|
||||
*/
|
||||
init() {
|
||||
this.initializeNameserverFormDisplay();
|
||||
this.initializeEventListeners();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines the initial display state of the nameserver form,
|
||||
* handling validation errors and setting visibility of elements accordingly.
|
||||
*/
|
||||
initializeNameserverFormDisplay() {
|
||||
|
||||
const domainName = document.getElementById('id_form-0-domain');
|
||||
if (domainName) {
|
||||
this.domain = domainName.value;
|
||||
} else {
|
||||
console.warn("Form expects a dom element, id_form-0-domain");
|
||||
}
|
||||
|
||||
// Check if exactly two nameserver forms exist: id_form-1-server is present but id_form-2-server is not
|
||||
const secondNameserver = document.getElementById('id_form-1-server');
|
||||
const thirdNameserver = document.getElementById('id_form-2-server'); // This should not exist
|
||||
|
||||
// Check if there are error messages in the form (indicated by elements with class 'usa-alert--error')
|
||||
const errorMessages = document.querySelectorAll('.usa-alert--error');
|
||||
|
||||
// This check indicates that there are exactly two forms (which is the case for the Add New Nameservers form)
|
||||
// and there is at least one error in the form. In this case, show the Add New Nameservers form, and
|
||||
// indicate that the form has changed
|
||||
if (this.addNameserversForm && secondNameserver && !thirdNameserver && errorMessages.length > 0) {
|
||||
showElement(this.addNameserversForm);
|
||||
this.formChanged = true;
|
||||
}
|
||||
|
||||
// This check indicates that there is either an Add New Nameservers form or an Add New Nameserver form
|
||||
// and that form has errors in it. In this case, show the form, and indicate that the form has
|
||||
// changed.
|
||||
if (this.addNameserversForm && this.addNameserversForm.querySelector('.usa-input--error')) {
|
||||
showElement(this.addNameserversForm);
|
||||
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('nameserver-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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// hide ip in forms unless nameserver ends with domain name
|
||||
let formIndex = 0;
|
||||
while (document.getElementById('id_form-' + formIndex + '-domain')) {
|
||||
let serverInput = document.getElementById('id_form-' + formIndex + '-server');
|
||||
let ipInput = document.getElementById('id_form-' + formIndex + '-ip');
|
||||
if (serverInput && ipInput) {
|
||||
let serverValue = serverInput.value.trim(); // Get the value and trim spaces
|
||||
let ipParent = ipInput.parentElement; // Get the parent element of ipInput
|
||||
|
||||
if (ipParent && !serverValue.endsWith('.' + this.domain)) {
|
||||
hideElement(ipParent); // Hide the parent element of ipInput
|
||||
}
|
||||
}
|
||||
formIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches event listeners to relevant UI elements for interaction handling.
|
||||
*/
|
||||
initializeEventListeners() {
|
||||
this.addNameserverButton.addEventListener('click', this.handleAddFormClick);
|
||||
|
||||
const editButtons = document.querySelectorAll('.nameserver-edit');
|
||||
editButtons.forEach(editButton => {
|
||||
editButton.addEventListener('click', this.handleEditClick);
|
||||
});
|
||||
|
||||
const cancelButtons = document.querySelectorAll('.nameserver-cancel');
|
||||
cancelButtons.forEach(cancelButton => {
|
||||
cancelButton.addEventListener('click', this.handleCancelClick);
|
||||
});
|
||||
|
||||
const cancelAddFormButtons = document.querySelectorAll('.nameserver-cancel-add-form');
|
||||
cancelAddFormButtons.forEach(cancelAddFormButton => {
|
||||
cancelAddFormButton.addEventListener('click', this.handleCancelAddFormClick);
|
||||
});
|
||||
|
||||
const deleteButtons = document.querySelectorAll('.nameserver-delete');
|
||||
deleteButtons.forEach(deleteButton => {
|
||||
deleteButton.addEventListener('click', this.handleDeleteClick);
|
||||
});
|
||||
|
||||
const deleteKebabButtons = document.querySelectorAll('.nameserver-delete-kebab');
|
||||
deleteKebabButtons.forEach(deleteKebabButton => {
|
||||
deleteKebabButton.addEventListener('click', this.handleDeleteKebabClick);
|
||||
});
|
||||
|
||||
const textInputs = document.querySelectorAll("input[type='text']");
|
||||
textInputs.forEach(input => {
|
||||
input.addEventListener("input", () => {
|
||||
this.formChanged = true;
|
||||
});
|
||||
});
|
||||
|
||||
// Add a specific listener for 'id_form-{number}-server' inputs to make
|
||||
// nameserver forms 'smart'. Inputs on server field will change the
|
||||
// display value of the associated IP address field.
|
||||
let formIndex = 0;
|
||||
while (document.getElementById(`id_form-${formIndex}-server`)) {
|
||||
let serverInput = document.getElementById(`id_form-${formIndex}-server`);
|
||||
let ipInput = document.getElementById(`id_form-${formIndex}-ip`);
|
||||
if (serverInput && ipInput) {
|
||||
let ipParent = ipInput.parentElement; // Get the parent element of ipInput
|
||||
let ipTd = ipParent.parentElement;
|
||||
// add an event listener on the server input that adjusts visibility
|
||||
// and value of the ip input (and its parent)
|
||||
serverInput.addEventListener("input", () => {
|
||||
let serverValue = serverInput.value.trim();
|
||||
if (ipParent && ipTd) {
|
||||
if (serverValue.endsWith('.' + this.domain)) {
|
||||
showElement(ipParent); // Show IP field if the condition matches
|
||||
ipTd.classList.add('width-40p');
|
||||
} else {
|
||||
hideElement(ipParent); // Hide IP field otherwise
|
||||
ipTd.classList.remove('width-40p');
|
||||
ipInput.value = ""; // Set the IP value to blank
|
||||
}
|
||||
} else {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
}
|
||||
});
|
||||
}
|
||||
formIndex++; // Move to the next index
|
||||
}
|
||||
|
||||
// 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 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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Nameserver' 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.addNameserversForm) {
|
||||
// Check if this.addNameserversForm is visible (i.e., does not have 'display-none')
|
||||
if (!this.addNameserversForm.classList.contains('display-none')) {
|
||||
this.resetAddNameserversForm();
|
||||
}
|
||||
// show nameservers form
|
||||
showElement(this.addNameserversForm);
|
||||
} else {
|
||||
this.addAlert("error", "You’ve reached the maximum amount of name server records (13). 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.addNameserversForm is visible (i.e., does not have 'display-none')
|
||||
if (this.addNameserversForm && !this.addNameserversForm.classList.contains('display-none')) {
|
||||
this.resetAddNameserversForm();
|
||||
}
|
||||
// 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 nameserver
|
||||
* after displaying modal and performing check for minimum number of nameservers.
|
||||
* @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 nameserver
|
||||
* after displaying modal and performing check for minimum number of nameservers.
|
||||
* @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 nameserver row after verifying the minimum required nameservers exist.
|
||||
* If there are only two nameservers left, deletion is prevented, and an alert is shown.
|
||||
* If deletion proceeds, the input fields are cleared, and the form is submitted.
|
||||
* @param {HTMLElement} editRow - The row corresponding to the nameserver being deleted.
|
||||
*/
|
||||
deleteRow(editRow) {
|
||||
// Check if at least two nameserver forms exist
|
||||
const fourthNameserver = document.getElementById('id_form-3-server'); // This should exist
|
||||
// This checks that at least 3 nameservers exist prior to the delete of a row, and if not
|
||||
// display an error alert
|
||||
if (fourthNameserver) {
|
||||
this.callback = () => {
|
||||
hideElement(editRow);
|
||||
let textInputs = editRow.querySelectorAll("input[type='text']");
|
||||
textInputs.forEach(input => {
|
||||
input.value = "";
|
||||
});
|
||||
document.querySelector("form").submit();
|
||||
};
|
||||
let modalTrigger = document.querySelector('#delete_trigger');
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
} else {
|
||||
this.addAlert("error", "At least two name servers are required. To proceed, add a new name server before removing this name server. If you need help, email us at help@get.gov.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event on the "Cancel" button in the add nameserver form.
|
||||
* Resets the form fields and hides the add form section.
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleCancelAddFormClick(event) {
|
||||
this.resetAddNameserversForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
if (editRow) {
|
||||
this.resetEditRowAndFormAndCollapseEditRow(editRow);
|
||||
} else {
|
||||
console.warn("Expected DOM element but did not find it");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Nameserver' form by clearing its input fields, removing errors,
|
||||
* and hiding the form to return it to its initial state.
|
||||
*/
|
||||
resetAddNameserversForm() {
|
||||
if (this.addNameserversForm) {
|
||||
// reset the values set in addNameserversForm
|
||||
this.resetInputValuesInElement(this.addNameserversForm);
|
||||
// remove errors from the addNameserversForm
|
||||
removeErrorsFromElement(this.addNameserversForm);
|
||||
// remove errors from the entire form
|
||||
removeFormErrors();
|
||||
// reset formChanged
|
||||
this.resetFormChanged();
|
||||
// hide the addNameserversForm
|
||||
hideElement(this.addNameserversForm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
let textInputs = domElement.querySelectorAll("input[type='text']");
|
||||
textInputs.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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 textInputs = editRow.querySelectorAll("input[type='text']");
|
||||
let tds = readOnlyRow.querySelectorAll("td");
|
||||
let updatedText = '';
|
||||
|
||||
// If a server name exists, store its value
|
||||
if (textInputs[0]) {
|
||||
updatedText = textInputs[0].value;
|
||||
}
|
||||
|
||||
// If an IP address exists, append it in parentheses next to the server name
|
||||
if (textInputs[1] && textInputs[1].value) {
|
||||
updatedText = updatedText + " (" + textInputs[1].value + ")";
|
||||
}
|
||||
|
||||
// Assign the formatted text to the first column of the readonly row
|
||||
if (tds[0]) {
|
||||
tds[0].innerText = updatedText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 NameserverForm when the DOM is fully loaded.
|
||||
*/
|
||||
export function initFormNameservers() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.getElementById('nameserver-add-button')) {
|
||||
const nameserverForm = new NameserverForm();
|
||||
nameserverForm.init();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* We will call this on the forms init, and also every time we add a form
|
||||
*
|
||||
*/
|
||||
function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
||||
function removeForm(e, formLabel, addButton, formIdentifier){
|
||||
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
|
||||
let formToRemove = e.target.closest(".repeatable-form");
|
||||
formToRemove.remove();
|
||||
|
@ -38,48 +38,7 @@ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
|||
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
||||
}
|
||||
|
||||
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
|
||||
// inject the USWDS required markup and make sure the INPUT is required
|
||||
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
|
||||
|
||||
// Remove the word optional
|
||||
innerSpan.textContent = innerSpan.textContent.replace(/\s*\(\s*optional\s*\)\s*/, '');
|
||||
|
||||
// Create a new element
|
||||
const newElement = document.createElement('abbr');
|
||||
newElement.textContent = '*';
|
||||
newElement.setAttribute("title", "required");
|
||||
newElement.classList.add("usa-hint", "usa-hint--required");
|
||||
|
||||
// Append the new element to the label
|
||||
node.appendChild(newElement);
|
||||
// Find the next sibling that is an input element
|
||||
let nextInputElement = node.nextElementSibling;
|
||||
|
||||
while (nextInputElement) {
|
||||
if (nextInputElement.tagName === 'INPUT') {
|
||||
// Found the next input element
|
||||
nextInputElement.setAttribute("required", "")
|
||||
break;
|
||||
}
|
||||
nextInputElement = nextInputElement.nextElementSibling;
|
||||
}
|
||||
nextInputElement.required = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Display the add more button if we have less than 13 forms
|
||||
if (isNameserversForm && forms.length <= 13) {
|
||||
addButton.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
if (isNameserversForm && forms.length < 3) {
|
||||
// Hide the delete buttons on the remaining nameservers
|
||||
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||
deleteButton.setAttribute("disabled", "true");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -131,7 +90,6 @@ function markForm(e, formLabel){
|
|||
*/
|
||||
function prepareNewDeleteButton(btn, formLabel) {
|
||||
let formIdentifier = "form"
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let addButton = document.querySelector("#add-form");
|
||||
|
||||
|
@ -144,7 +102,7 @@ function prepareNewDeleteButton(btn, formLabel) {
|
|||
} else {
|
||||
// We will remove the forms and re-order the formset
|
||||
btn.addEventListener('click', function(e) {
|
||||
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
|
||||
removeForm(e, formLabel, addButton, formIdentifier);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +115,6 @@ function prepareNewDeleteButton(btn, formLabel) {
|
|||
function prepareDeleteButtons(formLabel) {
|
||||
let formIdentifier = "form"
|
||||
let deleteButtons = document.querySelectorAll(".delete-record");
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let addButton = document.querySelector("#add-form");
|
||||
if (isOtherContactsForm) {
|
||||
|
@ -174,7 +131,7 @@ function prepareDeleteButtons(formLabel) {
|
|||
} else {
|
||||
// We will remove the forms and re-order the formset
|
||||
deleteButton.addEventListener('click', function(e) {
|
||||
removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier);
|
||||
removeForm(e, formLabel, addButton, formIdentifier);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -214,16 +171,14 @@ export function initFormsetsForms() {
|
|||
let addButton = document.querySelector("#add-form");
|
||||
let cloneIndex = 0;
|
||||
let formLabel = '';
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let isDsDataForm = document.querySelector(".ds-data-form");
|
||||
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
|
||||
// The Nameservers formset features 2 required and 11 optionals
|
||||
if (isNameserversForm) {
|
||||
// cloneIndex = 2;
|
||||
formLabel = "Name server";
|
||||
if( !(isOtherContactsForm || isDotgovDomain || isDsDataForm) ){
|
||||
return
|
||||
}
|
||||
// DNSSEC: DS Data
|
||||
} else if (isDsDataForm) {
|
||||
if (isDsDataForm) {
|
||||
formLabel = "DS data record";
|
||||
// The Other Contacts form
|
||||
} else if (isOtherContactsForm) {
|
||||
|
@ -235,11 +190,6 @@ export function initFormsetsForms() {
|
|||
}
|
||||
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
|
||||
|
||||
// On load: Disable the add more button if we have 13 forms
|
||||
if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) {
|
||||
addButton.setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
// Hide forms which have previously been deleted
|
||||
hideDeletedForms()
|
||||
|
||||
|
@ -258,33 +208,6 @@ export function initFormsetsForms() {
|
|||
// For the eample on Nameservers
|
||||
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
|
||||
|
||||
// Some Nameserver form checks since the delete can mess up the source object we're copying
|
||||
// in regards to required fields and hidden delete buttons
|
||||
if (isNameserversForm) {
|
||||
|
||||
// If the source element we're copying has required on an input,
|
||||
// reset that input
|
||||
let formRequiredNeedsCleanUp = newForm.innerHTML.includes('*');
|
||||
if (formRequiredNeedsCleanUp) {
|
||||
newForm.querySelector('label abbr').remove();
|
||||
// Get all input elements within the container
|
||||
const inputElements = newForm.querySelectorAll("input");
|
||||
// Loop through each input element and remove the 'required' attribute
|
||||
inputElements.forEach((input) => {
|
||||
if (input.hasAttribute("required")) {
|
||||
input.removeAttribute("required");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the source element we're copying has an disabled delete button,
|
||||
// enable that button
|
||||
let deleteButton= newForm.querySelector('.delete-record');
|
||||
if (deleteButton.hasAttribute("disabled")) {
|
||||
deleteButton.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
formNum++;
|
||||
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`);
|
||||
|
@ -304,16 +227,9 @@ export function initFormsetsForms() {
|
|||
let deleteButton = newForm.querySelector('button');
|
||||
deleteButton.setAttribute("aria-labelledby", header.id);
|
||||
deleteButton.setAttribute("aria-describedby", deleteDescription.id);
|
||||
} else {
|
||||
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
|
||||
// if indices 0 or 1 have been deleted
|
||||
let containsOptional = newForm.innerHTML.includes('(optional)');
|
||||
if (isNameserversForm && !containsOptional) {
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum} (optional)`);
|
||||
} else {
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
||||
}
|
||||
}
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
||||
newForm.innerHTML = newForm.innerHTML.replace(/\n/g, ''); // Remove newline characters
|
||||
newForm.innerHTML = newForm.innerHTML.replace(/>\s*</g, '><'); // Remove spaces between tags
|
||||
|
@ -369,20 +285,6 @@ export function initFormsetsForms() {
|
|||
let newDeleteButton = newForm.querySelector(".delete-record");
|
||||
if (newDeleteButton)
|
||||
prepareNewDeleteButton(newDeleteButton, formLabel);
|
||||
|
||||
// Disable the add more button if we have 13 forms
|
||||
if (isNameserversForm && formNum == 13) {
|
||||
addButton.setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
if (isNameserversForm && forms.length >= 2) {
|
||||
// Enable the delete buttons on the nameservers
|
||||
forms.forEach((form, index) => {
|
||||
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||
deleteButton.removeAttribute("disabled");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -408,22 +310,3 @@ export function triggerModalOnDsDataForm() {
|
|||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the delete buttons on nameserver forms on page load if < 3 forms
|
||||
*
|
||||
*/
|
||||
export function nameserversFormListener() {
|
||||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
if (isNameserversForm) {
|
||||
let forms = document.querySelectorAll(".repeatable-form");
|
||||
if (forms.length < 3) {
|
||||
// Hide the delete buttons on the 2 nameservers
|
||||
forms.forEach((form) => {
|
||||
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||
deleteButton.setAttribute("disabled", "true");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,19 +84,6 @@ export function getCsrfToken() {
|
|||
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to submit a form
|
||||
* @param {} form_id - the id of the form to be submitted
|
||||
*/
|
||||
export function submitForm(form_id) {
|
||||
let form = document.getElementById(form_id);
|
||||
if (form) {
|
||||
form.submit();
|
||||
} else {
|
||||
console.error("Form '" + form_id + "' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to strip HTML tags
|
||||
* THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { hookupYesNoListener, hookupRadioTogglerListener } from './radios.js';
|
||||
import { hookupYesNoListener } from './radios.js';
|
||||
import { initDomainValidators } from './domain-validators.js';
|
||||
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
||||
import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js';
|
||||
import { initFormNameservers } from './form-nameservers'
|
||||
import { initializeUrbanizationToggle } from './urbanization.js';
|
||||
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
||||
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
||||
|
@ -21,7 +22,7 @@ initDomainValidators();
|
|||
|
||||
initFormsetsForms();
|
||||
triggerModalOnDsDataForm();
|
||||
nameserversFormListener();
|
||||
initFormNameservers();
|
||||
|
||||
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
|
||||
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
|
||||
|
|
|
@ -190,6 +190,9 @@ abbr[title] {
|
|||
.visible-mobile-flex {
|
||||
display: none!important;
|
||||
}
|
||||
.text-right--tablet {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -286,3 +289,11 @@ Fit-content itself does not work.
|
|||
width: 3%;
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
|
||||
.width-40p {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.minh-143px {
|
||||
min-height: 143px;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,11 @@ th {
|
|||
border: none;
|
||||
}
|
||||
|
||||
td.padding-right-0,
|
||||
th.padding-right-0 {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
tr:first-child th:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,12 @@ class DomainNameserverForm(forms.Form):
|
|||
|
||||
domain = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
server = forms.CharField(label="Name server", strip=True)
|
||||
server = forms.CharField(
|
||||
label="Name server",
|
||||
strip=True,
|
||||
required=True,
|
||||
error_messages={"required": "At least two name servers are required."},
|
||||
)
|
||||
|
||||
ip = forms.CharField(
|
||||
label="IP address (IPv4 or IPv6)",
|
||||
|
@ -76,13 +81,6 @@ class DomainNameserverForm(forms.Form):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super(DomainNameserverForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# add custom error messages
|
||||
self.fields["server"].error_messages.update(
|
||||
{
|
||||
"required": "At least two name servers are required.",
|
||||
}
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
# clean is called from clean_forms, which is called from is_valid
|
||||
# after clean_fields. it is used to determine form level errors.
|
||||
|
@ -183,43 +181,83 @@ class DomainSuborganizationForm(forms.ModelForm):
|
|||
|
||||
class BaseNameserverFormset(forms.BaseFormSet):
|
||||
def clean(self):
|
||||
"""
|
||||
Check for duplicate entries in the formset.
|
||||
"""
|
||||
"""Check for duplicate entries in the formset and ensure at least two valid nameservers."""
|
||||
error_message = "At least two name servers are required."
|
||||
|
||||
# Check if there are at least two valid servers
|
||||
valid_servers_count = sum(
|
||||
1 for form in self.forms if form.cleaned_data.get("server") and form.cleaned_data.get("server").strip()
|
||||
)
|
||||
if valid_servers_count >= 2:
|
||||
# If there are, remove the "At least two name servers are required" error from each form
|
||||
# This will allow for successful submissions when the first or second entries are blanked
|
||||
# but there are enough entries total
|
||||
for form in self.forms:
|
||||
if form.errors.get("server") == ["At least two name servers are required."]:
|
||||
form.errors.pop("server")
|
||||
valid_forms, invalid_forms, empty_forms = self._categorize_forms(error_message)
|
||||
self._enforce_minimum_nameservers(valid_forms, invalid_forms, empty_forms, error_message)
|
||||
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid on its own
|
||||
if any(self.errors): # Skip further validation if individual forms already have errors
|
||||
return
|
||||
|
||||
data = []
|
||||
self._check_for_duplicates()
|
||||
|
||||
def _categorize_forms(self, error_message):
|
||||
"""Sort forms into valid, invalid or empty based on the 'server' field."""
|
||||
valid_forms = []
|
||||
invalid_forms = []
|
||||
empty_forms = []
|
||||
|
||||
for form in self.forms:
|
||||
if not self._is_server_validation_needed(form, error_message):
|
||||
invalid_forms.append(form)
|
||||
continue
|
||||
server = form.cleaned_data.get("server", "").strip()
|
||||
if server:
|
||||
valid_forms.append(form)
|
||||
else:
|
||||
empty_forms.append(form)
|
||||
|
||||
return valid_forms, invalid_forms, empty_forms
|
||||
|
||||
def _is_server_validation_needed(self, form, error_message):
|
||||
"""Determine if server validation should be performed on a given form."""
|
||||
return form.is_valid() or list(form.errors.get("server", [])) == [error_message]
|
||||
|
||||
def _enforce_minimum_nameservers(self, valid_forms, invalid_forms, empty_forms, error_message):
|
||||
"""Ensure at least two nameservers are provided, adjusting error messages as needed."""
|
||||
if len(valid_forms) + len(invalid_forms) < 2:
|
||||
self._add_required_error(empty_forms, error_message)
|
||||
else:
|
||||
self._remove_required_error_from_forms(error_message)
|
||||
|
||||
def _add_required_error(self, empty_forms, error_message):
|
||||
"""Add 'At least two name servers' error to one form and remove duplicates."""
|
||||
error_added = False
|
||||
|
||||
for form in empty_forms:
|
||||
if list(form.errors.get("server", [])) == [error_message]:
|
||||
form.errors.pop("server")
|
||||
|
||||
if not error_added:
|
||||
form.add_error("server", error_message)
|
||||
error_added = True
|
||||
|
||||
def _remove_required_error_from_forms(self, error_message):
|
||||
"""Remove the 'At least two name servers' error from all forms if sufficient nameservers exist."""
|
||||
for form in self.forms:
|
||||
if form.errors.get("server") == [error_message]:
|
||||
form.errors.pop("server")
|
||||
|
||||
def _check_for_duplicates(self):
|
||||
"""Ensure no duplicate nameservers exist within the formset."""
|
||||
seen_servers = set()
|
||||
duplicates = []
|
||||
|
||||
for index, form in enumerate(self.forms):
|
||||
if form.cleaned_data:
|
||||
value = form.cleaned_data["server"]
|
||||
# We need to make sure not to trigger the duplicate error in case the first and second nameservers
|
||||
# are empty. If there are enough records in the formset, that error is an unecessary blocker.
|
||||
# If there aren't, the required error will block the submit.
|
||||
if value in data and not (form.cleaned_data.get("server", "").strip() == "" and index == 1):
|
||||
for form in self.forms:
|
||||
if not form.cleaned_data:
|
||||
continue
|
||||
|
||||
server = form.cleaned_data["server"].strip()
|
||||
|
||||
if server and server in seen_servers:
|
||||
form.add_error(
|
||||
"server",
|
||||
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),
|
||||
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=server),
|
||||
)
|
||||
duplicates.append(value)
|
||||
duplicates.append(server)
|
||||
else:
|
||||
data.append(value)
|
||||
seen_servers.add(server)
|
||||
|
||||
|
||||
NameserverFormset = formset_factory(
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
{# messages block is under the back breadcrumb link #}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
|
|
@ -5,11 +5,22 @@
|
|||
|
||||
{% block domain_content %}
|
||||
|
||||
{# this is right after the messages block in the parent template #}
|
||||
{# this is right after the messages block in the parent template. #}
|
||||
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
{% if formset.initial|length >= formset.max_num %}
|
||||
<div class="usa-alert usa-alert--do-not-reset usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__text">
|
||||
You’ve reached the maximum amount of allowed name server records (13).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{% if portfolio %}
|
||||
<!-- Navigation breadcrumbs -->
|
||||
|
@ -32,82 +43,286 @@
|
|||
{% endif %}
|
||||
{% endblock breadcrumb %}
|
||||
|
||||
<h1>DNS name servers</h1>
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-6">
|
||||
<h1 class="tablet:margin-bottom-1">Name servers</h1>
|
||||
</div>
|
||||
|
||||
<div class="tablet:grid-col-6 text-right--tablet">
|
||||
<button type="button" class="usa-button margin-bottom-1 tablet:float-right" id="nameserver-add-button">
|
||||
Add name servers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
||||
|
||||
<p>Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You must add at least two name servers (13 max).</p>
|
||||
<p>Add a name server record by clicking “Add name servers.” You must add at least two name servers (13 max).</p>
|
||||
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<p class="margin-top-0">Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is “example.gov” and your name server is “ns1.example.gov,” then an IP address is required). Multiple IP addresses must be separated with commas.</p>
|
||||
<p class="margin-bottom-0">This step is uncommon unless you self-host your DNS or use custom addresses for your nameserver.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% comment %}
|
||||
This template supports the rendering of three different types of nameserver forms, conditionally displayed:
|
||||
1 - Add New Namervers form (rendered when there are no existing nameservers defined for the domain)
|
||||
2 - Nameserver table (rendered when the domain has existing nameservers, which can be viewed and edited)
|
||||
3 - Add New Nameserver (rendered above the Nameserver table to add a single additional nameserver)
|
||||
{% endcomment %}
|
||||
|
||||
{% if formset.initial and formset.forms.0.initial %}
|
||||
|
||||
{% comment %}This section renders both the Nameserver table and the Add New Nameserver {% endcomment %}
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--extra-large nameservers-form" method="post" novalidate id="form-container">
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate>
|
||||
{% 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-5">
|
||||
{% if forloop.last and not form.initial %}
|
||||
|
||||
{% comment %}
|
||||
This section renders the Add New Nameserver form.
|
||||
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">
|
||||
{{ form.domain }}
|
||||
<h2>Add a name server</h2>
|
||||
<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" %}
|
||||
{% 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_group_class="usa-form-group--unstyled-error" span_for_text=True %}
|
||||
{% with attr_required=True span_for_text=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error margin-top-2" %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with span_for_text=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-5">
|
||||
{% with label_text=form.ip.label sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" add_aria_label="Name server "|concat:forloop.counter|concat:" "|concat:form.ip.label %}
|
||||
<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 class="tablet:grid-col-2">
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075 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">Name server {{forloop.counter}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg>Add another name server
|
||||
</button>
|
||||
|
||||
{% comment %} Work around USWDS' button margins to add some spacing between the submit and the 'add more'
|
||||
This solution still works when we remove the 'add more' at 13 forms {% endcomment %}
|
||||
<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>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<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>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" role="columnheader">Name servers</th>
|
||||
<th scope="col" role="columnheader"><span class="sr-only">IP address</span></th>
|
||||
<th scope="col" role="columnheader" class="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 nameserver. Two rows are rendered, a readonly row
|
||||
and an edit row. Only one of which is displayed at a time.
|
||||
{% endcomment %}
|
||||
|
||||
{{ 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">
|
||||
<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">
|
||||
<use xlink:href="/public/img/sprite.svg#edit"></use>
|
||||
</svg>
|
||||
Edit <span class="usa-sr-only">{{ form.server.value }}</span>
|
||||
</button>
|
||||
|
||||
<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"
|
||||
>
|
||||
<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-0"
|
||||
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">
|
||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="more-actions-{{ form.server.value }}" 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-no-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"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Edit row -->
|
||||
<tr class="edit-row display-none">
|
||||
<td class="text-bottom">
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||
{% with attr_required=True add_initial_value_attr=True span_for_text=True add_group_class="usa-form-group--unstyled-error margin-top-0" %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="text-bottom">
|
||||
{% 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-0" %}
|
||||
{% input_with_errors form.ip %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td class="padding-right-0 text-bottom">
|
||||
<button class="usa-button usa-button--unstyled display-block margin-top-1" type="submit">Save</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--outline"
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled display-block nameserver-cancel"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the name server form to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled display-block text-secondary nameserver-delete"
|
||||
name="btn-delete-click"
|
||||
aria-label="Delete the name server from the registry"
|
||||
>Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% comment %}
|
||||
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">
|
||||
{% 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 %}
|
||||
{% 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>
|
||||
{% 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>
|
||||
|
||||
</section>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
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="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 name server?"
|
||||
aria-describedby="This will delete the name server from your DNS records. This action cannot be undone."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this name server?" modal_description="This will delete the name server from your DNS records. This action cannot be undone." modal_button_id="delete-click-button" modal_button_text="Yes, delete name server" modal_button_class="usa-button--secondary" %}
|
||||
</div>
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<!-- Banner for if_policy_acknowledged -->
|
||||
{% if form.is_policy_acknowledged.errors %}
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
{% for error in form.is_policy_acknowledged.errors %}
|
||||
<p class="usa-alert__text">{{ error }}</p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% if form.errors %}
|
||||
<div id="form-errors">
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert" tabindex="0">
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<span class="usa-sr-only">Error:</span>
|
||||
{{ error|escape }}
|
||||
|
@ -10,7 +10,7 @@
|
|||
{% endfor %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" tabindex="0">
|
||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<span class="usa-sr-only">Error:</span>
|
||||
{{ error|escape }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body {% if no_max_width %} maxw-none {% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
{% if cancel_button_text %}{{ cancel_button_text }}{% else %}Cancel{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
|
|
@ -16,7 +16,7 @@ Edit your User Profile |
|
|||
<div class="desktop:grid-col-8 desktop:grid-offset-2">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
|
|
@ -173,6 +173,10 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
if aria_labels:
|
||||
context["aria_label"] = " ".join(aria_labels)
|
||||
|
||||
# Conditionally add the data-initial-value attribute
|
||||
if context.get("add_initial_value_attr", False):
|
||||
attrs["data-initial-value"] = field.initial or ""
|
||||
|
||||
# ask Django to give us the widget dict
|
||||
# see Widget.get_context() on
|
||||
# https://docs.djangoproject.com/en/4.1/ref/forms/widgets
|
||||
|
|
|
@ -1449,7 +1449,7 @@ class MockEppLib(TestCase):
|
|||
)
|
||||
|
||||
infoDomainThreeHosts = fakedEppObject(
|
||||
"my-nameserver.gov",
|
||||
"threenameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
|
@ -1460,7 +1460,7 @@ class MockEppLib(TestCase):
|
|||
)
|
||||
|
||||
infoDomainFourHosts = fakedEppObject(
|
||||
"fournameserversDomain.gov",
|
||||
"fournameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
|
@ -1471,6 +1471,47 @@ class MockEppLib(TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
infoDomainTwelveHosts = fakedEppObject(
|
||||
"twelvenameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
"ns1.my-nameserver-1.com",
|
||||
"ns1.my-nameserver-2.com",
|
||||
"ns1.cats-are-superior3.com",
|
||||
"ns1.explosive-chicken-nuggets.com",
|
||||
"ns5.example.com",
|
||||
"ns6.example.com",
|
||||
"ns7.example.com",
|
||||
"ns8.example.com",
|
||||
"ns9.example.com",
|
||||
"ns10.example.com",
|
||||
"ns11.example.com",
|
||||
"ns12.example.com",
|
||||
],
|
||||
)
|
||||
|
||||
infoDomainThirteenHosts = fakedEppObject(
|
||||
"thirteennameserversdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
"ns1.my-nameserver-1.com",
|
||||
"ns1.my-nameserver-2.com",
|
||||
"ns1.cats-are-superior3.com",
|
||||
"ns1.explosive-chicken-nuggets.com",
|
||||
"ns5.example.com",
|
||||
"ns6.example.com",
|
||||
"ns7.example.com",
|
||||
"ns8.example.com",
|
||||
"ns9.example.com",
|
||||
"ns10.example.com",
|
||||
"ns11.example.com",
|
||||
"ns12.example.com",
|
||||
"ns13.example.com",
|
||||
],
|
||||
)
|
||||
|
||||
infoDomainNoHost = fakedEppObject(
|
||||
"my-nameserver.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
|
@ -1587,6 +1628,26 @@ class MockEppLib(TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
noNameserver = fakedEppObject(
|
||||
"nonameserver.com",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="securityContact",
|
||||
type=PublicContact.ContactTypeChoices.SECURITY,
|
||||
),
|
||||
common.DomainContact(
|
||||
contact="technicalContact",
|
||||
type=PublicContact.ContactTypeChoices.TECHNICAL,
|
||||
),
|
||||
common.DomainContact(
|
||||
contact="adminContact",
|
||||
type=PublicContact.ContactTypeChoices.ADMINISTRATIVE,
|
||||
),
|
||||
],
|
||||
hosts=[],
|
||||
)
|
||||
|
||||
infoDomainCheckHostIPCombo = fakedEppObject(
|
||||
"nameserversubdomain.gov",
|
||||
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||
|
@ -1801,10 +1862,13 @@ class MockEppLib(TestCase):
|
|||
"freeman.gov": (self.InfoDomainWithContacts, None),
|
||||
"threenameserversdomain.gov": (self.infoDomainThreeHosts, None),
|
||||
"fournameserversdomain.gov": (self.infoDomainFourHosts, None),
|
||||
"twelvenameserversdomain.gov": (self.infoDomainTwelveHosts, None),
|
||||
"thirteennameserversdomain.gov": (self.infoDomainThirteenHosts, None),
|
||||
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
|
||||
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
|
||||
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
|
||||
"justnameserver.com": (self.justNameserver, None),
|
||||
"nonameserver.com": (self.noNameserver, None),
|
||||
"meoward.gov": (self.mockDataInfoDomainSubdomain, None),
|
||||
"meow.gov": (self.mockDataInfoDomainSubdomainAndIPAddress, None),
|
||||
"fakemeow.gov": (self.mockDataInfoDomainNotSubdomainNoIP, None),
|
||||
|
|
|
@ -59,6 +59,7 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov")
|
||||
self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com")
|
||||
self.domain_no_nameserver, _ = Domain.objects.get_or_create(name="nonameserver.com")
|
||||
self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov")
|
||||
self.domain_on_hold, _ = Domain.objects.get_or_create(
|
||||
name="on-hold.gov",
|
||||
|
@ -84,17 +85,23 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
# We could simply use domain (igorville) but this will be more readable in tests
|
||||
# that inherit this setUp
|
||||
self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov")
|
||||
|
||||
self.domain_with_four_nameservers, _ = Domain.objects.get_or_create(name="fournameserversDomain.gov")
|
||||
self.domain_with_three_nameservers, _ = Domain.objects.get_or_create(name="threenameserversdomain.gov")
|
||||
self.domain_with_four_nameservers, _ = Domain.objects.get_or_create(name="fournameserversdomain.gov")
|
||||
self.domain_with_twelve_nameservers, _ = Domain.objects.get_or_create(name="twelvenameserversdomain.gov")
|
||||
self.domain_with_thirteen_nameservers, _ = Domain.objects.get_or_create(name="thirteennameserversdomain.gov")
|
||||
|
||||
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
|
||||
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_thirteen_nameservers)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_twelve_nameservers)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_four_nameservers)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_three_nameservers)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_no_nameserver)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dns_needed)
|
||||
|
@ -119,11 +126,26 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
domain=self.domain_dnssec_none,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_with_three_nameservers,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_with_four_nameservers,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_with_twelve_nameservers,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_with_thirteen_nameservers,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_with_ip,
|
||||
|
@ -134,6 +156,11 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
domain=self.domain_just_nameserver,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_no_nameserver,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
@ -1479,11 +1506,17 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_no_nameserver.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form with only one nameserver, should error
|
||||
# regarding required fields
|
||||
nameservers_page.form["form-0-server"] = "ns1.nonameserver.com"
|
||||
nameservers_page.form["form-0-ip"] = "127.0.0.1"
|
||||
nameservers_page.form["form-1-server"] = ""
|
||||
nameservers_page.form["form-1-ip"] = ""
|
||||
result = nameservers_page.form.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
|
||||
|
@ -1722,53 +1755,91 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
"""
|
||||
|
||||
nameserver1 = ""
|
||||
nameserver2 = "ns2.igorville.gov"
|
||||
nameserver3 = "ns3.igorville.gov"
|
||||
nameserver2 = "ns2.threenameserversdomain.gov"
|
||||
nameserver3 = "ns3.threenameserversdomain.gov"
|
||||
valid_ip = ""
|
||||
valid_ip_2 = "128.0.0.2"
|
||||
valid_ip_3 = "128.0.0.3"
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}))
|
||||
valid_ip_2 = "128.8.8.1"
|
||||
valid_ip_3 = "128.8.8.2"
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_three_nameservers.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
nameservers_page.form["form-0-ip"] = valid_ip
|
||||
nameservers_page.form["form-1-server"] = nameserver2
|
||||
nameservers_page.form["form-1-ip"] = valid_ip_2
|
||||
nameservers_page.form["form-2-server"] = nameserver3
|
||||
nameservers_page.form["form-2-ip"] = valid_ip_3
|
||||
result = nameservers_page.form.submit()
|
||||
|
||||
# webtest is not able to properly parse the form from nameservers_page, so manually
|
||||
# inputting form data
|
||||
form_data = {
|
||||
"csrfmiddlewaretoken": nameservers_page.form["csrfmiddlewaretoken"].value,
|
||||
"form-TOTAL_FORMS": "4",
|
||||
"form-INITIAL_FORMS": "3",
|
||||
"form-0-domain": "threenameserversdomain.gov",
|
||||
"form-0-server": nameserver1,
|
||||
"form-0-ip": valid_ip,
|
||||
"form-1-domain": "threenameserversdomain.gov",
|
||||
"form-1-server": nameserver2,
|
||||
"form-1-ip": valid_ip_2,
|
||||
"form-2-domain": "threenameserversdomain.gov",
|
||||
"form-2-server": nameserver3,
|
||||
"form-2-ip": valid_ip_3,
|
||||
"form-3-domain": "threenameserversdomain.gov",
|
||||
"form-3-server": "",
|
||||
"form-3-ip": "",
|
||||
}
|
||||
|
||||
result = self.app.post(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_three_nameservers.id}), form_data
|
||||
)
|
||||
|
||||
# form submission was a successful post, response should be a 302
|
||||
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_three_nameservers.id}),
|
||||
)
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page = result.follow()
|
||||
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
|
||||
|
||||
nameserver1 = "ns1.igorville.gov"
|
||||
nameserver1 = "ns1.threenameserversdomain.gov"
|
||||
nameserver2 = ""
|
||||
nameserver3 = "ns3.igorville.gov"
|
||||
nameserver3 = "ns3.threenameserversdomain.gov"
|
||||
valid_ip = "128.0.0.1"
|
||||
valid_ip_2 = ""
|
||||
valid_ip_3 = "128.0.0.3"
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
nameservers_page.form["form-0-ip"] = valid_ip
|
||||
nameservers_page.form["form-1-server"] = nameserver2
|
||||
nameservers_page.form["form-1-ip"] = valid_ip_2
|
||||
nameservers_page.form["form-2-server"] = nameserver3
|
||||
nameservers_page.form["form-2-ip"] = valid_ip_3
|
||||
result = nameservers_page.form.submit()
|
||||
|
||||
# webtest is not able to properly parse the form from nameservers_page, so manually
|
||||
# inputting form data
|
||||
form_data = {
|
||||
"csrfmiddlewaretoken": nameservers_page.form["csrfmiddlewaretoken"].value,
|
||||
"form-TOTAL_FORMS": "4",
|
||||
"form-INITIAL_FORMS": "3",
|
||||
"form-0-domain": "threenameserversdomain.gov",
|
||||
"form-0-server": nameserver1,
|
||||
"form-0-ip": valid_ip,
|
||||
"form-1-domain": "threenameserversdomain.gov",
|
||||
"form-1-server": nameserver2,
|
||||
"form-1-ip": valid_ip_2,
|
||||
"form-2-domain": "threenameserversdomain.gov",
|
||||
"form-2-server": nameserver3,
|
||||
"form-2-ip": valid_ip_3,
|
||||
"form-3-domain": "threenameserversdomain.gov",
|
||||
"form-3-server": "",
|
||||
"form-3-ip": "",
|
||||
}
|
||||
|
||||
result = self.app.post(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_three_nameservers.id}), form_data
|
||||
)
|
||||
|
||||
# form submission was a successful post, response should be a 302
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_three_nameservers.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
nameservers_page = result.follow()
|
||||
|
@ -1799,19 +1870,29 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Minimal check to ensure the form is loaded correctly
|
||||
self.assertEqual(nameservers_page.form["form-0-server"].value, "ns1.my-nameserver-1.com")
|
||||
self.assertEqual(nameservers_page.form["form-3-server"].value, "ns1.explosive-chicken-nuggets.com")
|
||||
# webtest is not able to properly parse the form from nameservers_page, so manually
|
||||
# inputting form data
|
||||
form_data = {
|
||||
"csrfmiddlewaretoken": nameservers_page.form["csrfmiddlewaretoken"].value,
|
||||
"form-TOTAL_FORMS": "4",
|
||||
"form-INITIAL_FORMS": "4",
|
||||
"form-0-domain": "fournameserversdomain.gov",
|
||||
"form-0-server": nameserver1,
|
||||
"form-0-ip": valid_ip,
|
||||
"form-1-domain": "fournameserversdomain.gov",
|
||||
"form-1-server": nameserver2,
|
||||
"form-1-ip": valid_ip_2,
|
||||
"form-2-domain": "fournameserversdomain.gov",
|
||||
"form-2-server": nameserver3,
|
||||
"form-2-ip": valid_ip_3,
|
||||
"form-3-domain": "fournameserversdomain.gov",
|
||||
"form-3-server": nameserver4,
|
||||
"form-3-ip": valid_ip_4,
|
||||
}
|
||||
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
nameservers_page.form["form-0-ip"] = valid_ip
|
||||
nameservers_page.form["form-1-server"] = nameserver2
|
||||
nameservers_page.form["form-1-ip"] = valid_ip_2
|
||||
nameservers_page.form["form-2-server"] = nameserver3
|
||||
nameservers_page.form["form-2-ip"] = valid_ip_3
|
||||
nameservers_page.form["form-3-server"] = nameserver4
|
||||
nameservers_page.form["form-3-ip"] = valid_ip_4
|
||||
result = nameservers_page.form.submit()
|
||||
result = self.app.post(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_four_nameservers.id}), form_data
|
||||
)
|
||||
|
||||
# form submission was a successful post, response should be a 302
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
@ -1823,6 +1904,34 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
nameservers_page = result.follow()
|
||||
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_nameservers_12_entries(self):
|
||||
"""Nameserver form does not present info alert when 12 enrties."""
|
||||
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_twelve_nameservers.id})
|
||||
)
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
self.assertNotContains(
|
||||
nameservers_page, "You’ve reached the maximum amount of allowed name server records (13)."
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_nameservers_13_entries(self):
|
||||
"""Nameserver form present3 info alert when 13 enrties."""
|
||||
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"domain_pk": self.domain_with_thirteen_nameservers.id})
|
||||
)
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
self.assertContains(nameservers_page, "You’ve reached the maximum amount of allowed name server records (13).")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_nameservers_form_invalid(self):
|
||||
"""Nameserver form does not submit with invalid data.
|
||||
|
@ -1837,12 +1946,12 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
|
|||
nameservers_page.form["form-0-server"] = ""
|
||||
result = nameservers_page.form.submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears four times, twice at the top of the page,
|
||||
# once around each required field.
|
||||
# error text appears twice, once at the top of the page,
|
||||
# once around the required field.
|
||||
self.assertContains(
|
||||
result,
|
||||
"At least two name servers are required.",
|
||||
count=4,
|
||||
count=2,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
|
|
@ -889,13 +889,12 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
"""The initial value for the form (which is a formset here)."""
|
||||
nameservers = self.object.nameservers
|
||||
initial_data = []
|
||||
|
||||
if nameservers is not None:
|
||||
# Add existing nameservers as initial data
|
||||
initial_data.extend({"server": name, "ip": ",".join(ip)} for name, ip in nameservers)
|
||||
|
||||
# Ensure at least 3 fields, filled or empty
|
||||
while len(initial_data) < 2:
|
||||
# Ensure 2 fields in the case we have no data
|
||||
if len(initial_data) == 0:
|
||||
initial_data.append({})
|
||||
|
||||
return initial_data
|
||||
|
@ -917,11 +916,6 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
|
||||
for i, form in enumerate(formset):
|
||||
form.fields["server"].label += f" {i+1}"
|
||||
if i < 2:
|
||||
form.fields["server"].required = True
|
||||
else:
|
||||
form.fields["server"].required = False
|
||||
form.fields["server"].label += " (optional)"
|
||||
form.fields["domain"].initial = self.object.name
|
||||
return formset
|
||||
|
||||
|
@ -933,8 +927,6 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
self._get_domain(request)
|
||||
formset = self.get_form()
|
||||
|
||||
logger.debug("got formet")
|
||||
|
||||
if "btn-cancel-click" in request.POST:
|
||||
url = self.get_success_url()
|
||||
return HttpResponseRedirect(url)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue