Skip to content

Commit

Permalink
Merge pull request #1200 from cisagov/dk/1016-nameservers-ui
Browse files Browse the repository at this point in the history
Issue 1016 - Nameserver UI (includes issues 919 and 1104)
  • Loading branch information
dave-kennedy-ecs authored Oct 30, 2023
2 parents 2be82f0 + c81a975 commit 228da2a
Show file tree
Hide file tree
Showing 14 changed files with 549 additions and 143 deletions.
4 changes: 1 addition & 3 deletions src/epplibwrapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
# Attn: these imports should NOT be at the top of the file
try:
from .client import CLIENT, commands
from .errors import RegistryError, ErrorCode, CANNOT_CONTACT_REGISTRY, GENERIC_ERROR
from .errors import RegistryError, ErrorCode
from epplib.models import common, info
from epplib.responses import extensions
from epplib import responses
Expand All @@ -61,6 +61,4 @@
"info",
"ErrorCode",
"RegistryError",
"CANNOT_CONTACT_REGISTRY",
"GENERIC_ERROR",
]
3 changes: 0 additions & 3 deletions src/epplibwrapper/errors.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from enum import IntEnum

CANNOT_CONTACT_REGISTRY = "Update failed. Cannot contact the registry."
GENERIC_ERROR = "Value entered was wrong."


class ErrorCode(IntEnum):
"""
Expand Down
132 changes: 76 additions & 56 deletions src/registrar/assets/js/get-gov.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,55 +231,31 @@ function handleValidationClick(e) {


/**
* An IIFE that attaches a click handler for our dynamic nameservers form
*
* Only does something on a single page, but it should be fast enough to run
* it everywhere.
* Prepare the namerservers and DS data forms delete buttons
* We will call this on the forms init, and also every time we add a form
*
*/
(function prepareNameserverForms() {
let serverForm = document.querySelectorAll(".server-form");
let container = document.querySelector("#form-container");
let addButton = document.querySelector("#add-nameserver-form");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");

let formNum = serverForm.length-1;
if (addButton)
addButton.addEventListener('click', addForm);

function addForm(e){
let newForm = serverForm[2].cloneNode(true);
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g');
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');

formNum++;
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`);
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`);
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`);
container.insertBefore(newForm, addButton);
newForm.querySelector("input").value = "";

totalForms.setAttribute('value', `${formNum+1}`);
}
})();

function prepareDeleteButtons() {
function prepareDeleteButtons(formLabel) {
let deleteButtons = document.querySelectorAll(".delete-record");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
let isNameserversForm = document.title.includes("DNS name servers |");
let addButton = document.querySelector("#add-form");

// Loop through each delete button and attach the click event listener
deleteButtons.forEach((deleteButton) => {
deleteButton.addEventListener('click', removeForm);
});

function removeForm(e){
let formToRemove = e.target.closest(".ds-record");
let formToRemove = e.target.closest(".repeatable-form");
formToRemove.remove();
let forms = document.querySelectorAll(".ds-record");
let forms = document.querySelectorAll(".repeatable-form");
totalForms.setAttribute('value', `${forms.length}`);

let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g');
// For the example on Nameservers
let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g');

forms.forEach((form, index) => {
// Iterate over child nodes of the current element
Expand All @@ -294,48 +270,88 @@ function prepareDeleteButtons() {
});
});

Array.from(form.querySelectorAll('h2, legend')).forEach((node) => {
node.textContent = node.textContent.replace(formLabelRegex, `DS Data record ${index + 1}`);
// h2 and legend for DS form, label for nameservers
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {

// Ticket: 1192
// if (isNameserversForm && index <= 1 && !node.innerHTML.includes('*')) {
// // Create a new element
// const newElement = document.createElement('abbr');
// newElement.textContent = '*';
// // TODO: finish building abbr

// // Append the new element to the parent
// 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
// console.log(nextInputElement);
// break;
// }
// nextInputElement = nextInputElement.nextElementSibling;
// }
// nextInputElement.required = true;
// }

// Ticket: 1192 - remove if
if (!(isNameserversForm && index <= 1)) {
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
}
});

// Display the add more button if we have less than 13 forms
if (isNameserversForm && forms.length <= 13) {
addButton.classList.remove("display-none")
}

});
}
}

/**
* An IIFE that attaches a click handler for our dynamic DNSSEC forms
* An IIFE that attaches a click handler for our dynamic formsets
*
* Only does something on a few pages, but it should be fast enough to run
* it everywhere.
*/
(function prepareDNSSECForms() {
let serverForm = document.querySelectorAll(".ds-record");
(function prepareFormsetsForms() {
let repeatableForm = document.querySelectorAll(".repeatable-form");
let container = document.querySelector("#form-container");
let addButton = document.querySelector("#add-ds-form");
let addButton = document.querySelector("#add-form");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
let cloneIndex = 0;
let formLabel = '';
let isNameserversForm = document.title.includes("DNS name servers |");
if (isNameserversForm) {
cloneIndex = 2;
formLabel = "Name server";
} else if ((document.title.includes("DS Data |")) || (document.title.includes("Key Data |"))) {
formLabel = "DS Data record";
}

// Attach click event listener on the delete buttons of the existing forms
prepareDeleteButtons();
prepareDeleteButtons(formLabel);

// Attack click event listener on the add button
if (addButton)
addButton.addEventListener('click', addForm);

/*
* Add a formset to the end of the form.
* For each element in the added formset, name the elements with the prefix,
* form-{#}-{element_name} where # is the index of the formset and element_name
* is the element's name.
* Additionally, update the form element's metadata, including totalForms' value.
*/
function addForm(e){
let forms = document.querySelectorAll(".ds-record");
let forms = document.querySelectorAll(".repeatable-form");
let formNum = forms.length;
let newForm = serverForm[0].cloneNode(true);
let newForm = repeatableForm[cloneIndex].cloneNode(true);
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g');
// For the eample on Nameservers
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');

formNum++;
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data record ${formNum}`);
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
container.insertBefore(newForm, addButton);

let inputs = newForm.querySelectorAll("input");
Expand Down Expand Up @@ -379,9 +395,13 @@ function prepareDeleteButtons() {
totalForms.setAttribute('value', `${formNum}`);

// Attach click event listener on the delete buttons of the new form
prepareDeleteButtons();
}
prepareDeleteButtons(formLabel);

// Hide the add more button if we have 13 forms
if (isNameserversForm && formNum == 13) {
addButton.classList.add("display-none")
}
}
})();

/**
Expand Down
4 changes: 4 additions & 0 deletions src/registrar/assets/sass/_theme/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
margin-top: units(3);
}

.usa-form .usa-button.margin-bottom-075 {
margin-bottom: units(1.5);
}

.usa-form .usa-button.margin-top-1 {
margin-top: units(1);
}
Expand Down
70 changes: 68 additions & 2 deletions src/registrar/forms/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
from django.forms import formset_factory

from phonenumber_field.widgets import RegionalPhoneNumberWidget
from registrar.utility.errors import (
NameserverError,
NameserverErrorCodes as nsErrorCodes,
)

from ..models import Contact, DomainInformation
from ..models import Contact, DomainInformation, Domain
from .common import (
ALGORITHM_CHOICES,
DIGEST_TYPE_CHOICES,
Expand All @@ -19,16 +23,78 @@ class DomainAddUserForm(forms.Form):
email = forms.EmailField(label="Email")


class IPAddressField(forms.CharField):
def validate(self, value):
super().validate(value) # Run the default CharField validation


class DomainNameserverForm(forms.Form):
"""Form for changing nameservers."""

domain = forms.CharField(widget=forms.HiddenInput, required=False)

server = forms.CharField(label="Name server", strip=True)
# when adding IPs to this form ensure they are stripped as well

ip = forms.CharField(label="IP Address (IPv4 or IPv6)", strip=True, required=False)

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.
# is_valid is typically called from view during a post
cleaned_data = super().clean()
self.clean_empty_strings(cleaned_data)
server = cleaned_data.get("server", "")
ip = cleaned_data.get("ip", None)
# remove ANY spaces in the ip field
ip = ip.replace(" ", "")
domain = cleaned_data.get("domain", "")

ip_list = self.extract_ip_list(ip)

if ip and not server and ip_list:
self.add_error("server", NameserverError(code=nsErrorCodes.MISSING_HOST))
elif server:
self.validate_nameserver_ip_combo(domain, server, ip_list)

return cleaned_data

def clean_empty_strings(self, cleaned_data):
ip = cleaned_data.get("ip", "")
if ip and len(ip.strip()) == 0:
cleaned_data["ip"] = None

def extract_ip_list(self, ip):
return [ip.strip() for ip in ip.split(",")] if ip else []

def validate_nameserver_ip_combo(self, domain, server, ip_list):
try:
Domain.checkHostIPCombo(domain, server, ip_list)
except NameserverError as e:
if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED:
self.add_error(
"server",
NameserverError(
code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED,
nameserver=domain,
ip=ip_list,
),
)
elif e.code == nsErrorCodes.MISSING_IP:
self.add_error(
"ip",
NameserverError(
code=nsErrorCodes.MISSING_IP, nameserver=domain, ip=ip_list
),
)
else:
self.add_error("ip", str(e))


NameserverFormset = formset_factory(
DomainNameserverForm,
extra=1,
max_num=13,
validate_max=True,
)


Expand Down
Loading

0 comments on commit 228da2a

Please sign in to comment.