mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-21 18:25:58 +02:00
Merge remote-tracking branch 'origin/main' into rjm/787-org-short-names
This commit is contained in:
commit
20157e2e99
42 changed files with 3702 additions and 872 deletions
|
@ -45,7 +45,7 @@ except NameError:
|
|||
# Attn: these imports should NOT be at the top of the file
|
||||
try:
|
||||
from .client import CLIENT, commands
|
||||
from .errors import RegistryError, ErrorCode
|
||||
from .errors import RegistryError, ErrorCode, CANNOT_CONTACT_REGISTRY, GENERIC_ERROR
|
||||
from epplib.models import common, info
|
||||
from epplib.responses import extensions
|
||||
from epplib import responses
|
||||
|
@ -61,4 +61,6 @@ __all__ = [
|
|||
"info",
|
||||
"ErrorCode",
|
||||
"RegistryError",
|
||||
"CANNOT_CONTACT_REGISTRY",
|
||||
"GENERIC_ERROR",
|
||||
]
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from enum import IntEnum
|
||||
|
||||
CANNOT_CONTACT_REGISTRY = "Update failed. Cannot contact the registry."
|
||||
GENERIC_ERROR = "Value entered was wrong."
|
||||
|
||||
|
||||
class ErrorCode(IntEnum):
|
||||
"""
|
||||
|
|
|
@ -236,28 +236,150 @@ function handleValidationClick(e) {
|
|||
* Only does something on a single page, but it should be fast enough to run
|
||||
* it everywhere.
|
||||
*/
|
||||
(function prepareForms() {
|
||||
let serverForm = document.querySelectorAll(".server-form")
|
||||
let container = document.querySelector("#form-container")
|
||||
let addButton = document.querySelector("#add-form")
|
||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
|
||||
(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
|
||||
addButton.addEventListener('click', addForm)
|
||||
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')
|
||||
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 = ""
|
||||
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}`)
|
||||
totalForms.setAttribute('value', `${formNum+1}`);
|
||||
}
|
||||
})();
|
||||
|
||||
function prepareDeleteButtons() {
|
||||
let deleteButtons = document.querySelectorAll(".delete-record");
|
||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
||||
|
||||
// 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");
|
||||
formToRemove.remove();
|
||||
let forms = document.querySelectorAll(".ds-record");
|
||||
totalForms.setAttribute('value', `${forms.length}`);
|
||||
|
||||
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
|
||||
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
|
||||
|
||||
forms.forEach((form, index) => {
|
||||
// Iterate over child nodes of the current element
|
||||
Array.from(form.querySelectorAll('label, input, select')).forEach((node) => {
|
||||
// Iterate through the attributes of the current node
|
||||
Array.from(node.attributes).forEach((attr) => {
|
||||
// Check if the attribute value matches the regex
|
||||
if (formNumberRegex.test(attr.value)) {
|
||||
// Replace the attribute value with the updated value
|
||||
attr.value = attr.value.replace(formNumberRegex, `form-${index}-`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Array.from(form.querySelectorAll('h2, legend')).forEach((node) => {
|
||||
node.textContent = node.textContent.replace(formLabelRegex, `DS Data record ${index + 1}`);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An IIFE that attaches a click handler for our dynamic DNSSEC forms
|
||||
*
|
||||
*/
|
||||
(function prepareDNSSECForms() {
|
||||
let serverForm = document.querySelectorAll(".ds-record");
|
||||
let container = document.querySelector("#form-container");
|
||||
let addButton = document.querySelector("#add-ds-form");
|
||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
||||
|
||||
// Attach click event listener on the delete buttons of the existing forms
|
||||
prepareDeleteButtons();
|
||||
|
||||
// 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 formNum = forms.length;
|
||||
let newForm = serverForm[0].cloneNode(true);
|
||||
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
|
||||
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
|
||||
|
||||
formNum++;
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data record ${formNum}`);
|
||||
container.insertBefore(newForm, addButton);
|
||||
|
||||
let inputs = newForm.querySelectorAll("input");
|
||||
// Reset the values of each input to blank
|
||||
inputs.forEach((input) => {
|
||||
input.classList.remove("usa-input--error");
|
||||
if (input.type === "text" || input.type === "number" || input.type === "password") {
|
||||
input.value = ""; // Set the value to an empty string
|
||||
|
||||
} else if (input.type === "checkbox" || input.type === "radio") {
|
||||
input.checked = false; // Uncheck checkboxes and radios
|
||||
}
|
||||
});
|
||||
|
||||
// Reset any existing validation classes
|
||||
let selects = newForm.querySelectorAll("select");
|
||||
selects.forEach((select) => {
|
||||
select.classList.remove("usa-input--error");
|
||||
select.selectedIndex = 0; // Set the value to an empty string
|
||||
});
|
||||
|
||||
let labels = newForm.querySelectorAll("label");
|
||||
labels.forEach((label) => {
|
||||
label.classList.remove("usa-label--error");
|
||||
});
|
||||
|
||||
let usaFormGroups = newForm.querySelectorAll(".usa-form-group");
|
||||
usaFormGroups.forEach((usaFormGroup) => {
|
||||
usaFormGroup.classList.remove("usa-form-group--error");
|
||||
});
|
||||
|
||||
// Remove any existing error messages
|
||||
let usaErrorMessages = newForm.querySelectorAll(".usa-error-message");
|
||||
usaErrorMessages.forEach((usaErrorMessage) => {
|
||||
let parentDiv = usaErrorMessage.closest('div');
|
||||
if (parentDiv) {
|
||||
parentDiv.remove(); // Remove the parent div if it exists
|
||||
}
|
||||
});
|
||||
|
||||
totalForms.setAttribute('value', `${formNum}`);
|
||||
|
||||
// Attach click event listener on the delete buttons of the new form
|
||||
prepareDeleteButtons();
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
@ -179,4 +179,4 @@ h1, h2, h3 {
|
|||
text-align: left;
|
||||
background: var(--primary);
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
}
|
||||
|
|
17
src/registrar/assets/sass/_theme/_alerts.scss
Normal file
17
src/registrar/assets/sass/_theme/_alerts.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Fixes some font size disparities with the Figma
|
||||
// for usa-alert alert elements
|
||||
.usa-alert {
|
||||
.usa-alert__heading.larger-font-sizing {
|
||||
font-size: units(3);
|
||||
}
|
||||
}
|
||||
|
||||
// The icon was off center for some reason
|
||||
// Fixes that issue
|
||||
@media (min-width: 64em){
|
||||
.usa-alert--warning{
|
||||
.usa-alert__body::before {
|
||||
left: 1rem !important;
|
||||
}
|
||||
}
|
||||
}
|
127
src/registrar/assets/sass/_theme/_base.scss
Normal file
127
src/registrar/assets/sass/_theme/_base.scss
Normal file
|
@ -0,0 +1,127 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
/* Styles for making visible to screen reader / AT users only. */
|
||||
.sr-only {
|
||||
@include sr-only;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
flex-grow: 1;
|
||||
padding-top: units(3);
|
||||
padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15
|
||||
}
|
||||
|
||||
#wrapper.dashboard {
|
||||
background-color: color('primary-lightest');
|
||||
padding-top: units(5);
|
||||
}
|
||||
|
||||
.usa-logo {
|
||||
@include at-media(desktop) {
|
||||
margin-top: units(2);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-logo__text {
|
||||
@include typeset('sans', 'xl', 2);
|
||||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
.usa-nav__primary {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.section--outlined {
|
||||
background-color: color('white');
|
||||
border: 1px solid color('base-lighter');
|
||||
border-radius: 4px;
|
||||
padding: 0 units(2) units(3);
|
||||
margin-top: units(3);
|
||||
|
||||
h2 {
|
||||
color: color('primary-dark');
|
||||
margin-top: units(2);
|
||||
margin-bottom: units(2);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@include at-media(mobile-lg) {
|
||||
margin-top: units(5);
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dotgov-status-box {
|
||||
background-color: color('primary-lightest');
|
||||
border-color: color('accent-cool-lighter');
|
||||
}
|
||||
|
||||
.dotgov-status-box--action-need {
|
||||
background-color: color('warning-lighter');
|
||||
border-color: color('warning');
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid color('primary-darker');
|
||||
}
|
||||
|
||||
.usa-footer__secondary-section {
|
||||
background-color: color('primary-lightest');
|
||||
}
|
||||
|
||||
.usa-footer__secondary-section a {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.usa-identifier__logo {
|
||||
height: units(7);
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
// workaround for underlining abbr element
|
||||
border-bottom: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@include at-media(tablet) {
|
||||
.float-right-tablet {
|
||||
float: right;
|
||||
}
|
||||
.float-left-tablet {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
@include at-media(desktop) {
|
||||
.float-right-desktop {
|
||||
float: right;
|
||||
}
|
||||
.float-left-desktop {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
align-items: flex-end;
|
||||
}
|
125
src/registrar/assets/sass/_theme/_buttons.scss
Normal file
125
src/registrar/assets/sass/_theme/_buttons.scss
Normal file
|
@ -0,0 +1,125 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
/* Make "placeholder" links visually obvious */
|
||||
a[href$="todo"]::after {
|
||||
background-color: yellow;
|
||||
color: color(blue-80v);
|
||||
content: " [link TBD]";
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a.breadcrumb__back {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
margin-bottom: units(2.5);
|
||||
&:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
@include at-media('tablet') {
|
||||
//align to top of sidebar
|
||||
margin-top: units(-0.5);
|
||||
}
|
||||
}
|
||||
|
||||
a.usa-button {
|
||||
text-decoration: none;
|
||||
color: color('white');
|
||||
}
|
||||
|
||||
a.usa-button:visited,
|
||||
a.usa-button:hover,
|
||||
a.usa-button:focus,
|
||||
a.usa-button:active {
|
||||
color: color('white');
|
||||
}
|
||||
|
||||
a.usa-button--outline,
|
||||
a.usa-button--outline:visited {
|
||||
box-shadow: inset 0 0 0 2px color('primary');
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
a.usa-button--outline:hover,
|
||||
a.usa-button--outline:focus {
|
||||
box-shadow: inset 0 0 0 2px color('primary-dark');
|
||||
color: color('primary-dark');
|
||||
}
|
||||
|
||||
a.usa-button--outline:active {
|
||||
box-shadow: inset 0 0 0 2px color('primary-darker');
|
||||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
a.withdraw {
|
||||
background-color: color('error');
|
||||
}
|
||||
|
||||
a.withdraw_outline,
|
||||
a.withdraw_outline:visited {
|
||||
box-shadow: inset 0 0 0 2px color('error');
|
||||
color: color('error');
|
||||
}
|
||||
|
||||
a.withdraw_outline:hover,
|
||||
a.withdraw_outline:focus {
|
||||
box-shadow: inset 0 0 0 2px color('error-dark');
|
||||
color: color('error-dark');
|
||||
}
|
||||
|
||||
a.withdraw_outline:active {
|
||||
box-shadow: inset 0 0 0 2px color('error-darker');
|
||||
color: color('error-darker');
|
||||
}
|
||||
|
||||
a.withdraw:hover,
|
||||
a.withdraw:focus {
|
||||
background-color: color('error-dark');
|
||||
}
|
||||
|
||||
a.withdraw:active {
|
||||
background-color: color('error-darker');
|
||||
}
|
||||
|
||||
.usa-button--unstyled .usa-icon {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
a.usa-button--unstyled:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.dotgov-button--green {
|
||||
background-color: color('success-dark');
|
||||
|
||||
&:hover {
|
||||
background-color: color('success-darker');
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color('green-80v');
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel button used on the
|
||||
// DNSSEC main page
|
||||
// We want to center this button on mobile
|
||||
// and add some extra left margin on tablet+
|
||||
.usa-button--cancel {
|
||||
text-align: center;
|
||||
@include at-media('tablet') {
|
||||
margin-left: units(2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// WARNING: crazy hack ahead:
|
||||
// Cancel button(s) on the DNSSEC form pages
|
||||
// We want to position the cancel button on the
|
||||
// dnssec forms next to the submit button
|
||||
// This button's markup is in its own form
|
||||
.btn-cancel {
|
||||
position: relative;
|
||||
top: -39.2px;
|
||||
left: 88px;
|
||||
}
|
10
src/registrar/assets/sass/_theme/_fieldsets.scss
Normal file
10
src/registrar/assets/sass/_theme/_fieldsets.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
fieldset {
|
||||
border: solid 1px color('base-lighter');
|
||||
padding: units(3);
|
||||
}
|
||||
|
||||
fieldset:not(:first-child) {
|
||||
margin-top: units(2);
|
||||
}
|
25
src/registrar/assets/sass/_theme/_forms.scss
Normal file
25
src/registrar/assets/sass/_theme/_forms.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
.usa-form .usa-button {
|
||||
margin-top: units(3);
|
||||
}
|
||||
|
||||
.usa-form--extra-large {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.usa-form--text-width {
|
||||
max-width: measure(5);
|
||||
}
|
||||
|
||||
.usa-textarea {
|
||||
@include at-media('tablet') {
|
||||
height: units('mobile');
|
||||
}
|
||||
}
|
||||
|
||||
.usa-form-group--unstyled-error {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
}
|
80
src/registrar/assets/sass/_theme/_register-form.scss
Normal file
80
src/registrar/assets/sass/_theme/_register-form.scss
Normal file
|
@ -0,0 +1,80 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "typography" as *;
|
||||
|
||||
.register-form-step > h1 {
|
||||
//align to top of sidebar on first page of the form
|
||||
margin-top: units(-1);
|
||||
}
|
||||
|
||||
//Tighter spacing when H2 is immediatly after H1
|
||||
.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
|
||||
.register-form-step h1 + h2 {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.register-form-step h3 {
|
||||
color: color('primary-dark');
|
||||
letter-spacing: $letter-space--xs;
|
||||
margin-top: units(3);
|
||||
margin-bottom: 0;
|
||||
|
||||
+ p {
|
||||
margin-top: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.register-form-step h4 {
|
||||
margin-bottom: 0;
|
||||
|
||||
+ p {
|
||||
margin-top: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.register-form-step a {
|
||||
color: color('primary');
|
||||
|
||||
&:visited {
|
||||
color: color('violet-70v'); //USWDS default
|
||||
}
|
||||
}
|
||||
.register-form-step .usa-form-group:first-of-type,
|
||||
.register-form-step .usa-label:first-of-type {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.ao_example p {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.domain_example {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.usa-list {
|
||||
margin-top: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.review__step {
|
||||
margin-top: units(3);
|
||||
}
|
||||
|
||||
.summary-item hr,
|
||||
.review__step hr {
|
||||
border: none; //reset
|
||||
border-top: 1px solid color('primary-dark');
|
||||
margin-top: 0;
|
||||
margin-bottom: units(0.5);
|
||||
}
|
||||
|
||||
.review__step__title a:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.review__step__name {
|
||||
color: color('primary-dark');
|
||||
font-weight: font-weight('semibold');
|
||||
margin-bottom: units(0.5);
|
||||
}
|
30
src/registrar/assets/sass/_theme/_sidenav.scss
Normal file
30
src/registrar/assets/sass/_theme/_sidenav.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
.usa-sidenav {
|
||||
.usa-sidenav__item {
|
||||
span {
|
||||
a.link_usa-checked {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav__step--locked {
|
||||
color: color('base-darker');
|
||||
span {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: units(1);
|
||||
|
||||
.usa-icon {
|
||||
flex-shrink: 0;
|
||||
//align lock body to x-height
|
||||
margin: units('2px') units(1) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stepnav {
|
||||
margin-top: units(2);
|
||||
}
|
93
src/registrar/assets/sass/_theme/_tables.scss
Normal file
93
src/registrar/assets/sass/_theme/_tables.scss
Normal file
|
@ -0,0 +1,93 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
.dotgov-table--stacked {
|
||||
td, th {
|
||||
padding: units(1) units(2) units(2px) 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
tr:first-child th:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: none;
|
||||
border-top: 2px solid color('base-light');
|
||||
margin-top: units(2);
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
td[data-label]:before,
|
||||
th[data-label]:before {
|
||||
color: color('primary-darker');
|
||||
padding-bottom: units(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.dotgov-table {
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: color('primary');
|
||||
|
||||
&:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.usa-icon {
|
||||
// align icon with x height
|
||||
margin-top: units(0.5);
|
||||
margin-right: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
|
||||
right: auto;
|
||||
}
|
||||
|
||||
tbody th {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@include at-media(mobile-lg) {
|
||||
|
||||
margin-top: units(1);
|
||||
|
||||
tr {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border-bottom: 1px solid color('base-light');
|
||||
}
|
||||
|
||||
thead th {
|
||||
color: color('primary-darker');
|
||||
border-bottom: 2px solid color('base-light');
|
||||
}
|
||||
|
||||
tbody tr:last-of-type {
|
||||
td, th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
td, th,
|
||||
.usa-tabel th{
|
||||
padding: units(2) units(2) units(2) 0;
|
||||
}
|
||||
|
||||
th:first-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
thead tr:first-child th:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
24
src/registrar/assets/sass/_theme/_typography.scss
Normal file
24
src/registrar/assets/sass/_theme/_typography.scss
Normal file
|
@ -0,0 +1,24 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
// Finer grained letterspacing adjustments
|
||||
$letter-space--xs: .0125em;
|
||||
|
||||
p,
|
||||
address,
|
||||
.usa-list li {
|
||||
@include typeset('sans', 'sm', 5);
|
||||
max-width: measure(5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
@include typeset('sans', '2xl', 2);
|
||||
margin: 0 0 units(2);
|
||||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: font-weight('semibold');
|
||||
line-height: line-height('heading', 3);
|
||||
margin: units(4) 0 units(1);
|
||||
color: color('primary-darker');
|
||||
}
|
|
@ -1,457 +0,0 @@
|
|||
/*
|
||||
* * * * * ==============================
|
||||
* * * * * ==============================
|
||||
* * * * * ==============================
|
||||
* * * * * ==============================
|
||||
========================================
|
||||
========================================
|
||||
========================================
|
||||
----------------------------------------
|
||||
USWDS THEME CUSTOM STYLES
|
||||
----------------------------------------
|
||||
!! Copy this file to your project's
|
||||
sass root. Don't edit the version
|
||||
in node_modules.
|
||||
----------------------------------------
|
||||
Custom project SASS goes here.
|
||||
|
||||
i.e.
|
||||
@include u-padding-right('05');
|
||||
----------------------------------------
|
||||
*/
|
||||
|
||||
// Finer grained letterspacing adjustments
|
||||
$letter-space--xs: .0125em;
|
||||
|
||||
@use "uswds-core" as *;
|
||||
|
||||
/* Styles for making visible to screen reader / AT users only. */
|
||||
.sr-only {
|
||||
@include sr-only;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.usa-logo {
|
||||
@include at-media(desktop) {
|
||||
margin-top: units(2);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-logo__text {
|
||||
@include typeset('sans', 'xl', 2);
|
||||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
.usa-nav__primary {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
p,
|
||||
address,
|
||||
.usa-list li {
|
||||
@include typeset('sans', 'sm', 5);
|
||||
max-width: measure(5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
@include typeset('sans', '2xl', 2);
|
||||
margin: 0 0 units(2);
|
||||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: font-weight('semibold');
|
||||
line-height: line-height('heading', 3);
|
||||
margin: units(4) 0 units(1);
|
||||
color: color('primary-darker');
|
||||
}
|
||||
|
||||
.register-form-step > h1 {
|
||||
//align to top of sidebar on first page of the form
|
||||
margin-top: units(-1);
|
||||
}
|
||||
|
||||
//Tighter spacing when H2 is immediatly after H1
|
||||
.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
|
||||
.register-form-step h1 + h2 {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.register-form-step h3 {
|
||||
color: color('primary-dark');
|
||||
letter-spacing: $letter-space--xs;
|
||||
margin-top: units(3);
|
||||
margin-bottom: 0;
|
||||
|
||||
+ p {
|
||||
margin-top: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.register-form-step h4 {
|
||||
margin-bottom: 0;
|
||||
|
||||
+ p {
|
||||
margin-top: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.register-form-step a {
|
||||
color: color('primary');
|
||||
|
||||
&:visited {
|
||||
color: color('violet-70v'); //USWDS default
|
||||
}
|
||||
}
|
||||
.register-form-step .usa-form-group:first-of-type,
|
||||
.register-form-step .usa-label:first-of-type {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
/* Make "placeholder" links visually obvious */
|
||||
a[href$="todo"]::after {
|
||||
background-color: yellow;
|
||||
color: color(blue-80v);
|
||||
content: " [link TBD]";
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a.breadcrumb__back {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
margin-bottom: units(2.5);
|
||||
&:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
@include at-media('tablet') {
|
||||
//align to top of sidebar
|
||||
margin-top: units(-0.5);
|
||||
}
|
||||
}
|
||||
|
||||
a.withdraw {
|
||||
background-color: color('error');
|
||||
}
|
||||
|
||||
a.withdraw_outline,
|
||||
a.withdraw_outline:visited {
|
||||
box-shadow: inset 0 0 0 2px color('error');
|
||||
color: color('error');
|
||||
}
|
||||
|
||||
a.withdraw_outline:hover,
|
||||
a.withdraw_outline:focus {
|
||||
box-shadow: inset 0 0 0 2px color('error-dark');
|
||||
color: color('error-dark');
|
||||
}
|
||||
|
||||
a.withdraw_outline:active {
|
||||
box-shadow: inset 0 0 0 2px color('error-darker');
|
||||
color: color('error-darker');
|
||||
}
|
||||
a.withdraw:hover,
|
||||
a.withdraw:focus {
|
||||
background-color: color('error-dark');
|
||||
}
|
||||
|
||||
a.withdraw:active {
|
||||
background-color: color('error-darker');
|
||||
}
|
||||
|
||||
.usa-sidenav {
|
||||
.usa-sidenav__item {
|
||||
span {
|
||||
a.link_usa-checked {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav__step--locked {
|
||||
color: color('base-darker');
|
||||
span {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: units(1);
|
||||
|
||||
.usa-icon {
|
||||
flex-shrink: 0;
|
||||
//align lock body to x-height
|
||||
margin: units('2px') units(1) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stepnav {
|
||||
margin-top: units(2);
|
||||
}
|
||||
|
||||
.ao_example p {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.domain_example {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.usa-list {
|
||||
margin-top: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.review__step {
|
||||
margin-top: units(3);
|
||||
}
|
||||
|
||||
.summary-item hr,
|
||||
.review__step hr {
|
||||
border: none; //reset
|
||||
border-top: 1px solid color('primary-dark');
|
||||
margin-top: 0;
|
||||
margin-bottom: units(0.5);
|
||||
}
|
||||
|
||||
.review__step__title a:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.review__step__name {
|
||||
color: color('primary-dark');
|
||||
font-weight: font-weight('semibold');
|
||||
margin-bottom: units(0.5);
|
||||
}
|
||||
|
||||
.usa-form .usa-button {
|
||||
margin-top: units(3);
|
||||
}
|
||||
|
||||
.usa-button--unstyled .usa-icon {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
a.usa-button--unstyled:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.dotgov-button--green {
|
||||
background-color: color('success-dark');
|
||||
|
||||
&:hover {
|
||||
background-color: color('success-darker');
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color('green-80v');
|
||||
}
|
||||
}
|
||||
|
||||
/** ---- DASHBOARD ---- */
|
||||
|
||||
#wrapper.dashboard {
|
||||
background-color: color('primary-lightest');
|
||||
padding-top: units(5);
|
||||
}
|
||||
|
||||
.section--outlined {
|
||||
background-color: color('white');
|
||||
border: 1px solid color('base-lighter');
|
||||
border-radius: 4px;
|
||||
padding: 0 units(2) units(3);
|
||||
margin-top: units(3);
|
||||
|
||||
h2 {
|
||||
color: color('primary-dark');
|
||||
margin-top: units(2);
|
||||
margin-bottom: units(2);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@include at-media(mobile-lg) {
|
||||
margin-top: units(5);
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dotgov-table--stacked {
|
||||
td, th {
|
||||
padding: units(1) units(2) units(2px) 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
tr:first-child th:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: none;
|
||||
border-top: 2px solid color('base-light');
|
||||
margin-top: units(2);
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
td[data-label]:before,
|
||||
th[data-label]:before {
|
||||
color: color('primary-darker');
|
||||
padding-bottom: units(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.dotgov-table {
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: color('primary');
|
||||
|
||||
&:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.usa-icon {
|
||||
// align icon with x height
|
||||
margin-top: units(0.5);
|
||||
margin-right: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
|
||||
right: auto;
|
||||
}
|
||||
|
||||
tbody th {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
@include at-media(mobile-lg) {
|
||||
|
||||
margin-top: units(1);
|
||||
|
||||
tr {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border-bottom: 1px solid color('base-light');
|
||||
}
|
||||
|
||||
thead th {
|
||||
color: color('primary-darker');
|
||||
border-bottom: 2px solid color('base-light');
|
||||
}
|
||||
|
||||
tbody tr:last-of-type {
|
||||
td, th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
td, th,
|
||||
.usa-tabel th{
|
||||
padding: units(2) units(2) units(2) 0;
|
||||
}
|
||||
|
||||
th:first-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
thead tr:first-child th:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dotgov-status-box {
|
||||
background-color: color('primary-lightest');
|
||||
border-color: color('accent-cool-lighter');
|
||||
}
|
||||
|
||||
.dotgov-status-box--action-need {
|
||||
background-color: color('warning-lighter');
|
||||
border-color: color('warning');
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
padding-top: units(3);
|
||||
padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
border-top: 1px solid color('primary-darker');
|
||||
}
|
||||
|
||||
.usa-footer__secondary-section {
|
||||
background-color: color('primary-lightest');
|
||||
}
|
||||
|
||||
.usa-footer__secondary-section a {
|
||||
color: color('primary');
|
||||
}
|
||||
|
||||
.usa-identifier__logo {
|
||||
height: units(7);
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
// workaround for underlining abbr element
|
||||
border-bottom: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.usa-textarea {
|
||||
@include at-media('tablet') {
|
||||
height: units('mobile');
|
||||
}
|
||||
}
|
||||
|
||||
// Fixes some font size disparities with the Figma
|
||||
// for usa-alert alert elements
|
||||
.usa-alert {
|
||||
.usa-alert__heading.larger-font-sizing {
|
||||
font-size: units(3);
|
||||
}
|
||||
}
|
||||
|
||||
// The icon was off center for some reason
|
||||
// Fixes that issue
|
||||
@media (min-width: 64em){
|
||||
.usa-alert--warning{
|
||||
.usa-alert__body::before {
|
||||
left: 1rem !important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,15 @@
|
|||
|
||||
/*--------------------------------------------------
|
||||
--- Custom Styles ---------------------------------*/
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@forward "base";
|
||||
@forward "typography";
|
||||
@forward "buttons";
|
||||
@forward "forms";
|
||||
@forward "fieldsets";
|
||||
@forward "alerts";
|
||||
@forward "tables";
|
||||
@forward "sidenav";
|
||||
@forward "register-form";
|
||||
|
||||
/*--------------------------------------------------
|
||||
--- Admin ---------------------------------*/
|
||||
|
|
|
@ -652,6 +652,9 @@ SESSION_COOKIE_SAMESITE = "Lax"
|
|||
# instruct browser to only send cookie via HTTPS
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
# session engine to cache session information
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
|
||||
# ~ Set by django.middleware.clickjacking.XFrameOptionsMiddleware
|
||||
# prevent clickjacking by instructing the browser not to load
|
||||
# our site within an iframe
|
||||
|
|
|
@ -81,9 +81,29 @@ urlpatterns = [
|
|||
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
||||
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||
path(
|
||||
"domain/<int:pk>/nameservers",
|
||||
"domain/<int:pk>/dns",
|
||||
views.DomainDNSView.as_view(),
|
||||
name="domain-dns",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/nameservers",
|
||||
views.DomainNameserversView.as_view(),
|
||||
name="domain-nameservers",
|
||||
name="domain-dns-nameservers",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec",
|
||||
views.DomainDNSSECView.as_view(),
|
||||
name="domain-dns-dnssec",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec/dsdata",
|
||||
views.DomainDsDataView.as_view(),
|
||||
name="domain-dns-dnssec-dsdata",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec/keydata",
|
||||
views.DomainKeyDataView.as_view(),
|
||||
name="domain-dns-dnssec-keydata",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/your-contact-information",
|
||||
|
|
|
@ -5,4 +5,9 @@ from .domain import (
|
|||
DomainSecurityEmailForm,
|
||||
DomainOrgNameAddressForm,
|
||||
ContactForm,
|
||||
DomainDnssecForm,
|
||||
DomainDsdataFormset,
|
||||
DomainDsdataForm,
|
||||
DomainKeydataFormset,
|
||||
DomainKeydataForm,
|
||||
)
|
||||
|
|
38
src/registrar/forms/common.py
Normal file
38
src/registrar/forms/common.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# common.py
|
||||
#
|
||||
# ALGORITHM_CHOICES are options for alg attribute in DS Data and Key Data
|
||||
# reference:
|
||||
# https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
|
||||
ALGORITHM_CHOICES = [
|
||||
(1, "(1) ERSA/MD5 [RSAMD5]"),
|
||||
(2, "(2) Diffie-Hellman [DH]"),
|
||||
(3, "(3) DSA/SHA-1 [DSA]"),
|
||||
(5, "(5) RSA/SHA-1 [RSASHA1]"),
|
||||
(6, "(6) DSA-NSEC3-SHA1"),
|
||||
(7, "(7) RSASHA1-NSEC3-SHA1"),
|
||||
(8, "(8) RSA/SHA-256 [RSASHA256]"),
|
||||
(10, "(10) RSA/SHA-512 [RSASHA512]"),
|
||||
(12, "(12) GOST R 34.10-2001 [ECC-GOST]"),
|
||||
(13, "(13) ECDSA Curve P-256 with SHA-256 [ECDSAP256SHA256]"),
|
||||
(14, "(14) ECDSA Curve P-384 with SHA-384 [ECDSAP384SHA384]"),
|
||||
(15, "(15) Ed25519"),
|
||||
(16, "(16) Ed448"),
|
||||
]
|
||||
# DIGEST_TYPE_CHOICES are options for digestType attribute in DS Data
|
||||
# reference: https://datatracker.ietf.org/doc/html/rfc4034#appendix-A.2
|
||||
DIGEST_TYPE_CHOICES = [
|
||||
(0, "(0) Reserved"),
|
||||
(1, "(1) SHA-256"),
|
||||
]
|
||||
# PROTOCOL_CHOICES are options for protocol attribute in Key Data
|
||||
# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.2
|
||||
PROTOCOL_CHOICES = [
|
||||
(3, "(3) DNSSEC"),
|
||||
]
|
||||
# FLAG_CHOICES are options for flags attribute in Key Data
|
||||
# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1
|
||||
FLAG_CHOICES = [
|
||||
(0, "(0)"),
|
||||
(256, "(256) ZSK"),
|
||||
(257, "(257) KSK"),
|
||||
]
|
|
@ -1,23 +1,27 @@
|
|||
"""Forms for domain management."""
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import RegexValidator
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
|
||||
from django.forms import formset_factory
|
||||
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
||||
from ..models import Contact, DomainInformation
|
||||
from .common import (
|
||||
ALGORITHM_CHOICES,
|
||||
DIGEST_TYPE_CHOICES,
|
||||
FLAG_CHOICES,
|
||||
PROTOCOL_CHOICES,
|
||||
)
|
||||
|
||||
|
||||
class DomainAddUserForm(forms.Form):
|
||||
|
||||
"""Form for adding a user to a domain."""
|
||||
|
||||
email = forms.EmailField(label="Email")
|
||||
|
||||
|
||||
class DomainNameserverForm(forms.Form):
|
||||
|
||||
"""Form for changing nameservers."""
|
||||
|
||||
server = forms.CharField(label="Name server", strip=True)
|
||||
|
@ -31,7 +35,6 @@ NameserverFormset = formset_factory(
|
|||
|
||||
|
||||
class ContactForm(forms.ModelForm):
|
||||
|
||||
"""Form for updating contacts."""
|
||||
|
||||
class Meta:
|
||||
|
@ -62,14 +65,12 @@ class ContactForm(forms.ModelForm):
|
|||
|
||||
|
||||
class DomainSecurityEmailForm(forms.Form):
|
||||
|
||||
"""Form for adding or editing a security email to a domain."""
|
||||
|
||||
security_email = forms.EmailField(label="Security email", required=False)
|
||||
|
||||
|
||||
class DomainOrgNameAddressForm(forms.ModelForm):
|
||||
|
||||
"""Form for updating the organization name and mailing address."""
|
||||
|
||||
zipcode = forms.CharField(
|
||||
|
@ -140,3 +141,91 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
|||
self.fields[field_name].required = True
|
||||
self.fields["state_territory"].widget.attrs.pop("maxlength", None)
|
||||
self.fields["zipcode"].widget.attrs.pop("maxlength", None)
|
||||
|
||||
|
||||
class DomainDnssecForm(forms.Form):
|
||||
"""Form for enabling and disabling dnssec"""
|
||||
|
||||
|
||||
class DomainDsdataForm(forms.Form):
|
||||
"""Form for adding or editing DNSSEC DS Data to a domain."""
|
||||
|
||||
key_tag = forms.IntegerField(
|
||||
required=True,
|
||||
label="Key tag",
|
||||
validators=[
|
||||
MinValueValidator(0, message="Value must be between 0 and 65535"),
|
||||
MaxValueValidator(65535, message="Value must be between 0 and 65535"),
|
||||
],
|
||||
error_messages={"required": ("Key tag is required.")},
|
||||
)
|
||||
|
||||
algorithm = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Algorithm",
|
||||
coerce=int, # need to coerce into int so dsData objects can be compared
|
||||
choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore
|
||||
error_messages={"required": ("Algorithm is required.")},
|
||||
)
|
||||
|
||||
digest_type = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Digest type",
|
||||
coerce=int, # need to coerce into int so dsData objects can be compared
|
||||
choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore
|
||||
error_messages={"required": ("Digest Type is required.")},
|
||||
)
|
||||
|
||||
digest = forms.CharField(
|
||||
required=True,
|
||||
label="Digest",
|
||||
error_messages={"required": ("Digest is required.")},
|
||||
)
|
||||
|
||||
|
||||
DomainDsdataFormset = formset_factory(
|
||||
DomainDsdataForm,
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
)
|
||||
|
||||
|
||||
class DomainKeydataForm(forms.Form):
|
||||
"""Form for adding or editing DNSSEC Key Data to a domain."""
|
||||
|
||||
flag = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Flag",
|
||||
coerce=int,
|
||||
choices=FLAG_CHOICES,
|
||||
error_messages={"required": ("Flag is required.")},
|
||||
)
|
||||
|
||||
protocol = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Protocol",
|
||||
coerce=int,
|
||||
choices=PROTOCOL_CHOICES,
|
||||
error_messages={"required": ("Protocol is required.")},
|
||||
)
|
||||
|
||||
algorithm = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Algorithm",
|
||||
coerce=int,
|
||||
choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore
|
||||
error_messages={"required": ("Algorithm is required.")},
|
||||
)
|
||||
|
||||
pub_key = forms.CharField(
|
||||
required=True,
|
||||
label="Pub key",
|
||||
error_messages={"required": ("Pub key is required.")},
|
||||
)
|
||||
|
||||
|
||||
DomainKeydataFormset = formset_factory(
|
||||
DomainKeydataForm,
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
)
|
||||
|
|
524
src/registrar/management/commands/load_transition_domain.py
Normal file
524
src/registrar/management/commands/load_transition_domain.py
Normal file
|
@ -0,0 +1,524 @@
|
|||
import sys
|
||||
import csv
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from registrar.models import TransitionDomain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class termColors:
|
||||
"""Colors for terminal outputs
|
||||
(makes reading the logs WAY easier)"""
|
||||
|
||||
HEADER = "\033[95m"
|
||||
OKBLUE = "\033[94m"
|
||||
OKCYAN = "\033[96m"
|
||||
OKGREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
FAIL = "\033[91m"
|
||||
ENDC = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
UNDERLINE = "\033[4m"
|
||||
BackgroundLightYellow = "\033[103m"
|
||||
|
||||
|
||||
def query_yes_no(question: str, default="yes") -> bool:
|
||||
"""Ask a yes/no question via raw_input() and return their answer.
|
||||
|
||||
"question" is a string that is presented to the user.
|
||||
"default" is the presumed answer if the user just hits <Enter>.
|
||||
It must be "yes" (the default), "no" or None (meaning
|
||||
an answer is required of the user).
|
||||
|
||||
The "answer" return value is True for "yes" or False for "no".
|
||||
"""
|
||||
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
prompt = " [Y/n] "
|
||||
elif default == "no":
|
||||
prompt = " [y/N] "
|
||||
else:
|
||||
raise ValueError("invalid default answer: '%s'" % default)
|
||||
|
||||
while True:
|
||||
logger.info(question + prompt)
|
||||
choice = input().lower()
|
||||
if default is not None and choice == "":
|
||||
return valid[default]
|
||||
elif choice in valid:
|
||||
return valid[choice]
|
||||
else:
|
||||
logger.info("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Loads data for domains that are in transition
|
||||
(populates transition_domain model objects)."""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add our three filename arguments (in order: domain contacts,
|
||||
contacts, and domain statuses)
|
||||
OPTIONAL ARGUMENTS:
|
||||
--sep
|
||||
The default delimiter is set to "|", but may be changed using --sep
|
||||
--debug
|
||||
A boolean (default to true), which activates additional print statements
|
||||
--limitParse
|
||||
Used to set a limit for the number of data entries to insert. Set to 0
|
||||
(or just don't use this argument) to parse every entry.
|
||||
--resetTable
|
||||
Use this to trigger a prompt for deleting all table entries. Useful
|
||||
for testing purposes, but USE WITH CAUTION
|
||||
"""
|
||||
parser.add_argument(
|
||||
"domain_contacts_filename", help="Data file with domain contact information"
|
||||
)
|
||||
parser.add_argument(
|
||||
"contacts_filename",
|
||||
help="Data file with contact information",
|
||||
)
|
||||
parser.add_argument(
|
||||
"domain_statuses_filename", help="Data file with domain status information"
|
||||
)
|
||||
|
||||
parser.add_argument("--sep", default="|", help="Delimiter character")
|
||||
|
||||
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
|
||||
|
||||
parser.add_argument(
|
||||
"--limitParse", default=0, help="Sets max number of entries to load"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--resetTable",
|
||||
help="Deletes all data in the TransitionDomain table",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
)
|
||||
|
||||
def print_debug_mode_statements(
|
||||
self, debug_on: bool, debug_max_entries_to_parse: int
|
||||
):
|
||||
"""Prints additional terminal statements to indicate if --debug
|
||||
or --limitParse are in use"""
|
||||
if debug_on:
|
||||
logger.info(
|
||||
f"""{termColors.OKCYAN}
|
||||
----------DEBUG MODE ON----------
|
||||
Detailed print statements activated.
|
||||
{termColors.ENDC}
|
||||
"""
|
||||
)
|
||||
if debug_max_entries_to_parse > 0:
|
||||
logger.info(
|
||||
f"""{termColors.OKCYAN}
|
||||
----------LIMITER ON----------
|
||||
Parsing of entries will be limited to
|
||||
{debug_max_entries_to_parse} lines per file.")
|
||||
Detailed print statements activated.
|
||||
{termColors.ENDC}
|
||||
"""
|
||||
)
|
||||
|
||||
def get_domain_user_dict(
|
||||
self, domain_statuses_filename: str, sep: str
|
||||
) -> defaultdict[str, str]:
|
||||
"""Creates a mapping of domain name -> status"""
|
||||
domain_status_dictionary = defaultdict(str)
|
||||
logger.info("Reading domain statuses data file %s", domain_statuses_filename)
|
||||
with open(domain_statuses_filename, "r") as domain_statuses_file: # noqa
|
||||
for row in csv.reader(domain_statuses_file, delimiter=sep):
|
||||
domainName = row[0].lower()
|
||||
domainStatus = row[1].lower()
|
||||
domain_status_dictionary[domainName] = domainStatus
|
||||
logger.info("Loaded statuses for %d domains", len(domain_status_dictionary))
|
||||
return domain_status_dictionary
|
||||
|
||||
def get_user_emails_dict(
|
||||
self, contacts_filename: str, sep
|
||||
) -> defaultdict[str, str]:
|
||||
"""Creates mapping of userId -> emails"""
|
||||
user_emails_dictionary = defaultdict(str)
|
||||
logger.info("Reading domain-contacts data file %s", contacts_filename)
|
||||
with open(contacts_filename, "r") as contacts_file:
|
||||
for row in csv.reader(contacts_file, delimiter=sep):
|
||||
user_id = row[0]
|
||||
user_email = row[6]
|
||||
user_emails_dictionary[user_id] = user_email
|
||||
logger.info("Loaded emails for %d users", len(user_emails_dictionary))
|
||||
return user_emails_dictionary
|
||||
|
||||
def get_mapped_status(self, status_to_map: str):
|
||||
"""
|
||||
Given a verisign domain status, return a corresponding
|
||||
status defined for our domains.
|
||||
|
||||
We map statuses as follows;
|
||||
"serverHold” fields will map to hold, clientHold to hold
|
||||
and any ok state should map to Ready.
|
||||
"""
|
||||
status_maps = {
|
||||
"hold": TransitionDomain.StatusChoices.ON_HOLD,
|
||||
"serverhold": TransitionDomain.StatusChoices.ON_HOLD,
|
||||
"clienthold": TransitionDomain.StatusChoices.ON_HOLD,
|
||||
"created": TransitionDomain.StatusChoices.READY,
|
||||
"ok": TransitionDomain.StatusChoices.READY,
|
||||
}
|
||||
mapped_status = status_maps.get(status_to_map)
|
||||
return mapped_status
|
||||
|
||||
def print_summary_duplications(
|
||||
self,
|
||||
duplicate_domain_user_combos: list[TransitionDomain],
|
||||
duplicate_domains: list[TransitionDomain],
|
||||
users_without_email: list[str],
|
||||
):
|
||||
"""Called at the end of the script execution to print out a summary of
|
||||
data anomalies in the imported Verisign data. Currently, we check for:
|
||||
- duplicate domains
|
||||
- duplicate domain - user pairs
|
||||
- any users without e-mails (this would likely only happen if the contacts
|
||||
file is missing a user found in the domain_contacts file)
|
||||
"""
|
||||
total_duplicate_pairs = len(duplicate_domain_user_combos)
|
||||
total_duplicate_domains = len(duplicate_domains)
|
||||
total_users_without_email = len(users_without_email)
|
||||
if total_users_without_email > 0:
|
||||
users_without_email_as_string = "{}".format(
|
||||
", ".join(map(str, duplicate_domain_user_combos))
|
||||
)
|
||||
logger.warning(
|
||||
f"{termColors.YELLOW} No e-mails found for users: {users_without_email_as_string}" # noqa
|
||||
)
|
||||
if total_duplicate_pairs > 0 or total_duplicate_domains > 0:
|
||||
duplicate_pairs_as_string = "{}".format(
|
||||
", ".join(map(str, duplicate_domain_user_combos))
|
||||
)
|
||||
duplicate_domains_as_string = "{}".format(
|
||||
", ".join(map(str, duplicate_domains))
|
||||
)
|
||||
logger.warning(
|
||||
f"""{termColors.YELLOW}
|
||||
|
||||
----DUPLICATES FOUND-----
|
||||
|
||||
{total_duplicate_pairs} DOMAIN - USER pairs
|
||||
were NOT unique in the supplied data files;
|
||||
|
||||
{duplicate_pairs_as_string}
|
||||
|
||||
{total_duplicate_domains} DOMAINS were NOT unique in
|
||||
the supplied data files;
|
||||
|
||||
{duplicate_domains_as_string}
|
||||
{termColors.ENDC}"""
|
||||
)
|
||||
|
||||
def print_summary_status_findings(
|
||||
self, domains_without_status: list[str], outlier_statuses: list[str]
|
||||
):
|
||||
"""Called at the end of the script execution to print out a summary of
|
||||
status anomolies in the imported Verisign data. Currently, we check for:
|
||||
- domains without a status
|
||||
- any statuses not accounted for in our status mappings (see
|
||||
get_mapped_status() function)
|
||||
"""
|
||||
total_domains_without_status = len(domains_without_status)
|
||||
total_outlier_statuses = len(outlier_statuses)
|
||||
if total_domains_without_status > 0:
|
||||
domains_without_status_as_string = "{}".format(
|
||||
", ".join(map(str, domains_without_status))
|
||||
)
|
||||
logger.warning(
|
||||
f"""{termColors.YELLOW}
|
||||
|
||||
--------------------------------------------
|
||||
Found {total_domains_without_status} domains
|
||||
without a status (defaulted to READY)
|
||||
---------------------------------------------
|
||||
|
||||
{domains_without_status_as_string}
|
||||
{termColors.ENDC}"""
|
||||
)
|
||||
|
||||
if total_outlier_statuses > 0:
|
||||
domains_without_status_as_string = "{}".format(
|
||||
", ".join(map(str, outlier_statuses))
|
||||
) # noqa
|
||||
logger.warning(
|
||||
f"""{termColors.YELLOW}
|
||||
|
||||
--------------------------------------------
|
||||
Found {total_outlier_statuses} unaccounted
|
||||
for statuses-
|
||||
--------------------------------------------
|
||||
|
||||
No mappings found for the following statuses
|
||||
(defaulted to Ready):
|
||||
|
||||
{domains_without_status_as_string}
|
||||
{termColors.ENDC}"""
|
||||
)
|
||||
|
||||
def print_debug(self, print_condition: bool, print_statement: str):
|
||||
"""This function reduces complexity of debug statements
|
||||
in other functions.
|
||||
It uses the logger to write the given print_statement to the
|
||||
terminal if print_condition is TRUE"""
|
||||
# DEBUG:
|
||||
if print_condition:
|
||||
logger.info(print_statement)
|
||||
|
||||
def prompt_table_reset(self):
|
||||
"""Brings up a prompt in the terminal asking
|
||||
if the user wishes to delete data in the
|
||||
TransitionDomain table. If the user confirms,
|
||||
deletes all the data in the TransitionDomain table"""
|
||||
confirm_reset = query_yes_no(
|
||||
f"""
|
||||
{termColors.FAIL}
|
||||
WARNING: Resetting the table will permanently delete all
|
||||
the data!
|
||||
Are you sure you want to continue?{termColors.ENDC}"""
|
||||
)
|
||||
if confirm_reset:
|
||||
logger.info(
|
||||
f"""{termColors.YELLOW}
|
||||
----------Clearing Table Data----------
|
||||
(please wait)
|
||||
{termColors.ENDC}"""
|
||||
)
|
||||
TransitionDomain.objects.all().delete()
|
||||
|
||||
def handle( # noqa: C901
|
||||
self,
|
||||
domain_contacts_filename,
|
||||
contacts_filename,
|
||||
domain_statuses_filename,
|
||||
**options,
|
||||
):
|
||||
"""Parse the data files and create TransitionDomains."""
|
||||
sep = options.get("sep")
|
||||
|
||||
# If --resetTable was used, prompt user to confirm
|
||||
# deletion of table data
|
||||
if options.get("resetTable"):
|
||||
self.prompt_table_reset()
|
||||
|
||||
# Get --debug argument
|
||||
debug_on = options.get("debug")
|
||||
|
||||
# Get --LimitParse argument
|
||||
debug_max_entries_to_parse = int(
|
||||
options.get("limitParse")
|
||||
) # set to 0 to parse all entries
|
||||
|
||||
# print message to terminal about which args are in use
|
||||
self.print_debug_mode_statements(debug_on, debug_max_entries_to_parse)
|
||||
|
||||
# STEP 1:
|
||||
# Create mapping of domain name -> status
|
||||
domain_status_dictionary = self.get_domain_user_dict(
|
||||
domain_statuses_filename, sep
|
||||
)
|
||||
|
||||
# STEP 2:
|
||||
# Create mapping of userId -> email
|
||||
user_emails_dictionary = self.get_user_emails_dict(contacts_filename, sep)
|
||||
|
||||
# STEP 3:
|
||||
# Parse the domain_contacts file and create TransitionDomain objects,
|
||||
# using the dictionaries from steps 1 & 2 to lookup needed information.
|
||||
|
||||
to_create = []
|
||||
|
||||
# keep track of statuses that don't match our available
|
||||
# status values
|
||||
outlier_statuses = []
|
||||
|
||||
# keep track of domains that have no known status
|
||||
domains_without_status = []
|
||||
|
||||
# keep track of users that have no e-mails
|
||||
users_without_email = []
|
||||
|
||||
# keep track of duplications..
|
||||
duplicate_domains = []
|
||||
duplicate_domain_user_combos = []
|
||||
|
||||
# keep track of domains we ADD or UPDATE
|
||||
total_updated_domain_entries = 0
|
||||
total_new_entries = 0
|
||||
|
||||
# if we are limiting our parse (for testing purposes, keep
|
||||
# track of total rows parsed)
|
||||
total_rows_parsed = 0
|
||||
|
||||
# Start parsing the main file and create TransitionDomain objects
|
||||
logger.info("Reading domain-contacts data file %s", domain_contacts_filename)
|
||||
with open(domain_contacts_filename, "r") as domain_contacts_file:
|
||||
for row in csv.reader(domain_contacts_file, delimiter=sep):
|
||||
total_rows_parsed += 1
|
||||
|
||||
# fields are just domain, userid, role
|
||||
# lowercase the domain names
|
||||
new_entry_domain_name = row[0].lower()
|
||||
user_id = row[1]
|
||||
|
||||
new_entry_status = TransitionDomain.StatusChoices.READY
|
||||
new_entry_email = ""
|
||||
new_entry_emailSent = False # set to False by default
|
||||
|
||||
# PART 1: Get the status
|
||||
if new_entry_domain_name not in domain_status_dictionary:
|
||||
# This domain has no status...default to "Create"
|
||||
# (For data analysis purposes, add domain name
|
||||
# to list of all domains without status
|
||||
# (avoid duplicate entries))
|
||||
if new_entry_domain_name not in domains_without_status:
|
||||
domains_without_status.append(new_entry_domain_name)
|
||||
else:
|
||||
# Map the status
|
||||
original_status = domain_status_dictionary[new_entry_domain_name]
|
||||
mapped_status = self.get_mapped_status(original_status)
|
||||
if mapped_status is None:
|
||||
# (For data analysis purposes, check for any statuses
|
||||
# that don't have a mapping and add to list
|
||||
# of "outlier statuses")
|
||||
logger.info("Unknown status: " + original_status)
|
||||
outlier_statuses.append(original_status)
|
||||
else:
|
||||
new_entry_status = mapped_status
|
||||
|
||||
# PART 2: Get the e-mail
|
||||
if user_id not in user_emails_dictionary:
|
||||
# this user has no e-mail...this should never happen
|
||||
if user_id not in users_without_email:
|
||||
users_without_email.append(user_id)
|
||||
else:
|
||||
new_entry_email = user_emails_dictionary[user_id]
|
||||
|
||||
# PART 3: Create the transition domain object
|
||||
# Check for duplicate data in the file we are
|
||||
# parsing so we do not add duplicates
|
||||
# NOTE: Currently, we allow duplicate domains,
|
||||
# but not duplicate domain-user pairs.
|
||||
# However, track duplicate domains for now,
|
||||
# since we are still deciding on whether
|
||||
# to make this field unique or not. ~10/25/2023
|
||||
existing_domain = next(
|
||||
(x for x in to_create if x.domain_name == new_entry_domain_name),
|
||||
None,
|
||||
)
|
||||
existing_domain_user_pair = next(
|
||||
(
|
||||
x
|
||||
for x in to_create
|
||||
if x.username == new_entry_email
|
||||
and x.domain_name == new_entry_domain_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing_domain is not None:
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"{termColors.YELLOW} DUPLICATE file entries found for domain: {new_entry_domain_name} {termColors.ENDC}", # noqa
|
||||
)
|
||||
if new_entry_domain_name not in duplicate_domains:
|
||||
duplicate_domains.append(new_entry_domain_name)
|
||||
if existing_domain_user_pair is not None:
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"""{termColors.YELLOW} DUPLICATE file entries found for domain - user {termColors.BackgroundLightYellow} PAIR {termColors.ENDC}{termColors.YELLOW}:
|
||||
{new_entry_domain_name} - {new_entry_email} {termColors.ENDC}""", # noqa
|
||||
)
|
||||
if existing_domain_user_pair not in duplicate_domain_user_combos:
|
||||
duplicate_domain_user_combos.append(existing_domain_user_pair)
|
||||
else:
|
||||
entry_exists = TransitionDomain.objects.filter(
|
||||
username=new_entry_email, domain_name=new_entry_domain_name
|
||||
).exists()
|
||||
if entry_exists:
|
||||
try:
|
||||
existing_entry = TransitionDomain.objects.get(
|
||||
username=new_entry_email,
|
||||
domain_name=new_entry_domain_name,
|
||||
)
|
||||
|
||||
if existing_entry.status != new_entry_status:
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"{termColors.OKCYAN}"
|
||||
f"Updating entry: {existing_entry}"
|
||||
f"Status: {existing_entry.status} > {new_entry_status}" # noqa
|
||||
f"Email Sent: {existing_entry.email_sent} > {new_entry_emailSent}" # noqa
|
||||
f"{termColors.ENDC}",
|
||||
)
|
||||
existing_entry.status = new_entry_status
|
||||
existing_entry.email_sent = new_entry_emailSent
|
||||
existing_entry.save()
|
||||
except TransitionDomain.MultipleObjectsReturned:
|
||||
logger.info(
|
||||
f"{termColors.FAIL}"
|
||||
f"!!! ERROR: duplicate entries exist in the"
|
||||
f"transtion_domain table for domain:"
|
||||
f"{new_entry_domain_name}"
|
||||
f"----------TERMINATING----------"
|
||||
)
|
||||
sys.exit()
|
||||
else:
|
||||
# no matching entry, make one
|
||||
new_entry = TransitionDomain(
|
||||
username=new_entry_email,
|
||||
domain_name=new_entry_domain_name,
|
||||
status=new_entry_status,
|
||||
email_sent=new_entry_emailSent,
|
||||
)
|
||||
to_create.append(new_entry)
|
||||
total_new_entries += 1
|
||||
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"{termColors.OKCYAN} Adding entry {total_new_entries}: {new_entry} {termColors.ENDC}", # noqa
|
||||
)
|
||||
|
||||
# Check Parse limit and exit loop if needed
|
||||
if (
|
||||
total_rows_parsed >= debug_max_entries_to_parse
|
||||
and debug_max_entries_to_parse != 0
|
||||
):
|
||||
logger.info(
|
||||
f"{termColors.YELLOW}"
|
||||
f"----PARSE LIMIT REACHED. HALTING PARSER.----"
|
||||
f"{termColors.ENDC}"
|
||||
)
|
||||
break
|
||||
|
||||
TransitionDomain.objects.bulk_create(to_create)
|
||||
|
||||
logger.info(
|
||||
f"""{termColors.OKGREEN}
|
||||
============= FINISHED ===============
|
||||
Created {total_new_entries} transition domain entries,
|
||||
updated {total_updated_domain_entries} transition domain entries
|
||||
{termColors.ENDC}
|
||||
"""
|
||||
)
|
||||
|
||||
# Print a summary of findings (duplicate entries,
|
||||
# missing data..etc.)
|
||||
self.print_summary_duplications(
|
||||
duplicate_domain_user_combos, duplicate_domains, users_without_email
|
||||
)
|
||||
self.print_summary_status_findings(domains_without_status, outlier_statuses)
|
|
@ -0,0 +1,409 @@
|
|||
import logging
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from django_fsm import TransitionNotAllowed # type: ignore
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from registrar.models import TransitionDomain
|
||||
from registrar.models import Domain
|
||||
from registrar.models import DomainInvitation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class termColors:
|
||||
"""Colors for terminal outputs
|
||||
(makes reading the logs WAY easier)"""
|
||||
|
||||
HEADER = "\033[95m"
|
||||
OKBLUE = "\033[94m"
|
||||
OKCYAN = "\033[96m"
|
||||
OKGREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
FAIL = "\033[91m"
|
||||
ENDC = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
UNDERLINE = "\033[4m"
|
||||
BackgroundLightYellow = "\033[103m"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Load data from transition domain tables
|
||||
into main domain tables. Also create domain invitation
|
||||
entries for every domain we ADD (but not for domains
|
||||
we UPDATE)"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
|
||||
|
||||
parser.add_argument(
|
||||
"--limitParse",
|
||||
default=0,
|
||||
help="Sets max number of entries to load, set to 0 to load all entries",
|
||||
)
|
||||
|
||||
def print_debug_mode_statements(
|
||||
self, debug_on: bool, debug_max_entries_to_parse: int
|
||||
):
|
||||
"""Prints additional terminal statements to indicate if --debug
|
||||
or --limitParse are in use"""
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"""{termColors.OKCYAN}
|
||||
----------DEBUG MODE ON----------
|
||||
Detailed print statements activated.
|
||||
{termColors.ENDC}
|
||||
""",
|
||||
)
|
||||
self.print_debug(
|
||||
debug_max_entries_to_parse > 0,
|
||||
f"""{termColors.OKCYAN}
|
||||
----------LIMITER ON----------
|
||||
Parsing of entries will be limited to
|
||||
{debug_max_entries_to_parse} lines per file.")
|
||||
Detailed print statements activated.
|
||||
{termColors.ENDC}
|
||||
""",
|
||||
)
|
||||
|
||||
def print_debug(self, print_condition: bool, print_statement: str):
|
||||
"""This function reduces complexity of debug statements
|
||||
in other functions.
|
||||
It uses the logger to write the given print_statement to the
|
||||
terminal if print_condition is TRUE"""
|
||||
# DEBUG:
|
||||
if print_condition:
|
||||
logger.info(print_statement)
|
||||
|
||||
def update_domain_status(
|
||||
self, transition_domain: TransitionDomain, target_domain: Domain, debug_on: bool
|
||||
) -> bool:
|
||||
"""Given a transition domain that matches an existing domain,
|
||||
updates the existing domain object with that status of
|
||||
the transition domain.
|
||||
Returns TRUE if an update was made. FALSE if the states
|
||||
matched and no update was made"""
|
||||
|
||||
transition_domain_status = transition_domain.status
|
||||
existing_status = target_domain.state
|
||||
if transition_domain_status != existing_status:
|
||||
if transition_domain_status == TransitionDomain.StatusChoices.ON_HOLD:
|
||||
target_domain.place_client_hold(ignoreEPP=True)
|
||||
else:
|
||||
target_domain.revert_client_hold(ignoreEPP=True)
|
||||
target_domain.save()
|
||||
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"""{termColors.YELLOW}
|
||||
>> Updated {target_domain.name} state from
|
||||
'{existing_status}' to '{target_domain.state}'
|
||||
(no domain invitation entry added)
|
||||
{termColors.ENDC}""",
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def print_summary_of_findings(
|
||||
self,
|
||||
domains_to_create,
|
||||
updated_domain_entries,
|
||||
domain_invitations_to_create,
|
||||
skipped_domain_entries,
|
||||
debug_on,
|
||||
):
|
||||
"""Prints to terminal a summary of findings from
|
||||
transferring transition domains to domains"""
|
||||
|
||||
total_new_entries = len(domains_to_create)
|
||||
total_updated_domain_entries = len(updated_domain_entries)
|
||||
total_domain_invitation_entries = len(domain_invitations_to_create)
|
||||
|
||||
logger.info(
|
||||
f"""{termColors.OKGREEN}
|
||||
============= FINISHED ===============
|
||||
Created {total_new_entries} transition domain entries,
|
||||
Updated {total_updated_domain_entries} transition domain entries
|
||||
|
||||
Created {total_domain_invitation_entries} domain invitation entries
|
||||
(NOTE: no invitations are SENT in this script)
|
||||
{termColors.ENDC}
|
||||
"""
|
||||
)
|
||||
if len(skipped_domain_entries) > 0:
|
||||
logger.info(
|
||||
f"""{termColors.FAIL}
|
||||
============= SKIPPED DOMAINS (ERRORS) ===============
|
||||
{skipped_domain_entries}
|
||||
{termColors.ENDC}
|
||||
"""
|
||||
)
|
||||
|
||||
# determine domainInvitations we SKIPPED
|
||||
skipped_domain_invitations = []
|
||||
for domain in domains_to_create:
|
||||
skipped_domain_invitations.append(domain)
|
||||
for domain_invite in domain_invitations_to_create:
|
||||
if domain_invite.domain in skipped_domain_invitations:
|
||||
skipped_domain_invitations.remove(domain_invite.domain)
|
||||
if len(skipped_domain_invitations) > 0:
|
||||
logger.info(
|
||||
f"""{termColors.FAIL}
|
||||
============= SKIPPED DOMAIN INVITATIONS (ERRORS) ===============
|
||||
{skipped_domain_invitations}
|
||||
{termColors.ENDC}
|
||||
"""
|
||||
)
|
||||
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"""{termColors.YELLOW}
|
||||
|
||||
Created Domains:
|
||||
{domains_to_create}
|
||||
|
||||
Updated Domains:
|
||||
{updated_domain_entries}
|
||||
|
||||
{termColors.ENDC}
|
||||
""",
|
||||
)
|
||||
|
||||
def try_add_domain_invitation(
|
||||
self, domain_email: str, associated_domain: Domain
|
||||
) -> DomainInvitation | None:
|
||||
"""If no domain invitation exists for the given domain and
|
||||
e-mail, create and return a new domain invitation object.
|
||||
If one already exists, or if the email is invalid, return NONE"""
|
||||
|
||||
# this should never happen, but adding it just in case
|
||||
if associated_domain is None:
|
||||
logger.warning(
|
||||
f"""
|
||||
{termColors.FAIL}
|
||||
!!! ERROR: Domain cannot be null for a
|
||||
Domain Invitation object!
|
||||
|
||||
RECOMMENDATION:
|
||||
Somehow, an empty domain object is
|
||||
being passed to the subroutine in charge
|
||||
of making domain invitations. Walk through
|
||||
the code to see what is amiss.
|
||||
|
||||
----------TERMINATING----------"""
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
# check that the given e-mail is valid
|
||||
if domain_email is not None and domain_email != "":
|
||||
# check that a domain invitation doesn't already
|
||||
# exist for this e-mail / Domain pair
|
||||
domain_email_already_in_domain_invites = DomainInvitation.objects.filter(
|
||||
email=domain_email.lower(), domain=associated_domain
|
||||
).exists()
|
||||
if not domain_email_already_in_domain_invites:
|
||||
# Create new domain invitation
|
||||
new_domain_invitation = DomainInvitation(
|
||||
email=domain_email.lower(), domain=associated_domain
|
||||
)
|
||||
return new_domain_invitation
|
||||
return None
|
||||
|
||||
def handle(
|
||||
self,
|
||||
**options,
|
||||
):
|
||||
"""Parse entries in TransitionDomain table
|
||||
and create (or update) corresponding entries in the
|
||||
Domain and DomainInvitation tables."""
|
||||
|
||||
# grab command line arguments and store locally...
|
||||
debug_on = options.get("debug")
|
||||
debug_max_entries_to_parse = int(
|
||||
options.get("limitParse")
|
||||
) # set to 0 to parse all entries
|
||||
|
||||
self.print_debug_mode_statements(debug_on, debug_max_entries_to_parse)
|
||||
|
||||
# domains to ADD
|
||||
domains_to_create = []
|
||||
domain_invitations_to_create = []
|
||||
# domains we UPDATED
|
||||
updated_domain_entries = []
|
||||
# domains we SKIPPED
|
||||
skipped_domain_entries = []
|
||||
# if we are limiting our parse (for testing purposes, keep
|
||||
# track of total rows parsed)
|
||||
total_rows_parsed = 0
|
||||
|
||||
logger.info(
|
||||
f"""{termColors.OKGREEN}
|
||||
==========================
|
||||
Beginning Data Transfer
|
||||
==========================
|
||||
{termColors.ENDC}"""
|
||||
)
|
||||
|
||||
for transition_domain in TransitionDomain.objects.all():
|
||||
transition_domain_name = transition_domain.domain_name
|
||||
transition_domain_status = transition_domain.status
|
||||
transition_domain_email = transition_domain.username
|
||||
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"""{termColors.OKCYAN}
|
||||
Processing Transition Domain: {transition_domain_name}, {transition_domain_status}, {transition_domain_email}
|
||||
{termColors.ENDC}""", # noqa
|
||||
)
|
||||
|
||||
new_domain_invitation = None
|
||||
# Check for existing domain entry
|
||||
domain_exists = Domain.objects.filter(name=transition_domain_name).exists()
|
||||
if domain_exists:
|
||||
try:
|
||||
# get the existing domain
|
||||
domain_to_update = Domain.objects.get(name=transition_domain_name)
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"""{termColors.YELLOW}
|
||||
> Found existing entry in Domain table for: {transition_domain_name}, {domain_to_update.state}
|
||||
{termColors.ENDC}""", # noqa
|
||||
)
|
||||
|
||||
# for existing entry, update the status to
|
||||
# the transition domain status
|
||||
update_made = self.update_domain_status(
|
||||
transition_domain, domain_to_update, debug_on
|
||||
)
|
||||
if update_made:
|
||||
# keep track of updated domains for data analysis purposes
|
||||
updated_domain_entries.append(transition_domain.domain_name)
|
||||
|
||||
# check if we need to add a domain invitation
|
||||
# (eg. for a new user)
|
||||
new_domain_invitation = self.try_add_domain_invitation(
|
||||
transition_domain_email, domain_to_update
|
||||
)
|
||||
|
||||
except Domain.MultipleObjectsReturned:
|
||||
# This exception was thrown once before during testing.
|
||||
# While the circumstances that led to corrupt data in
|
||||
# the domain table was a freak accident, and the possibility of it
|
||||
# happening again is safe-guarded by a key constraint,
|
||||
# better to keep an eye out for it since it would require
|
||||
# immediate attention.
|
||||
logger.warning(
|
||||
f"""
|
||||
{termColors.FAIL}
|
||||
!!! ERROR: duplicate entries already exist in the
|
||||
Domain table for the following domain:
|
||||
{transition_domain_name}
|
||||
|
||||
RECOMMENDATION:
|
||||
This means the Domain table is corrupt. Please
|
||||
check the Domain table data as there should be a key
|
||||
constraint which prevents duplicate entries.
|
||||
|
||||
----------TERMINATING----------"""
|
||||
)
|
||||
sys.exit()
|
||||
except TransitionNotAllowed as err:
|
||||
skipped_domain_entries.append(transition_domain_name)
|
||||
logger.warning(
|
||||
f"""{termColors.FAIL}
|
||||
Unable to change state for {transition_domain_name}
|
||||
|
||||
RECOMMENDATION:
|
||||
This indicates there might have been changes to the
|
||||
Domain model which were not accounted for in this
|
||||
migration script. Please check state change rules
|
||||
in the Domain model and ensure we are following the
|
||||
correct state transition pathways.
|
||||
|
||||
INTERNAL ERROR MESSAGE:
|
||||
'TRANSITION NOT ALLOWED' exception
|
||||
{err}
|
||||
----------SKIPPING----------"""
|
||||
)
|
||||
else:
|
||||
# no entry was found in the domain table
|
||||
# for the given domain. Create a new entry.
|
||||
|
||||
# first see if we are already adding an entry for this domain.
|
||||
# The unique key constraint does not allow duplicate domain entries
|
||||
# even if there are different users.
|
||||
existing_domain_in_to_create = next(
|
||||
(x for x in domains_to_create if x.name == transition_domain_name),
|
||||
None,
|
||||
)
|
||||
if existing_domain_in_to_create is not None:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"""{termColors.YELLOW}
|
||||
Duplicate Detected: {transition_domain_name}.
|
||||
Cannot add duplicate entry for another username.
|
||||
Violates Unique Key constraint.
|
||||
|
||||
Checking for unique user e-mail for Domain Invitations...
|
||||
{termColors.ENDC}""",
|
||||
)
|
||||
new_domain_invitation = self.try_add_domain_invitation(
|
||||
transition_domain_email, existing_domain_in_to_create
|
||||
)
|
||||
else:
|
||||
# no matching entry, make one
|
||||
new_domain = Domain(
|
||||
name=transition_domain_name, state=transition_domain_status
|
||||
)
|
||||
domains_to_create.append(new_domain)
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"{termColors.OKCYAN} Adding domain: {new_domain} {termColors.ENDC}", # noqa
|
||||
)
|
||||
new_domain_invitation = self.try_add_domain_invitation(
|
||||
transition_domain_email, new_domain
|
||||
)
|
||||
|
||||
if new_domain_invitation is None:
|
||||
logger.info(
|
||||
f"{termColors.YELLOW} ! No new e-mail detected !" # noqa
|
||||
f"(SKIPPED ADDING DOMAIN INVITATION){termColors.ENDC}"
|
||||
)
|
||||
else:
|
||||
# DEBUG:
|
||||
self.print_debug(
|
||||
debug_on,
|
||||
f"{termColors.OKCYAN} Adding domain invitation: {new_domain_invitation} {termColors.ENDC}", # noqa
|
||||
)
|
||||
domain_invitations_to_create.append(new_domain_invitation)
|
||||
|
||||
# Check parse limit and exit loop if parse limit has been reached
|
||||
if (
|
||||
debug_max_entries_to_parse > 0
|
||||
and total_rows_parsed >= debug_max_entries_to_parse
|
||||
):
|
||||
logger.info(
|
||||
f"""{termColors.YELLOW}
|
||||
----PARSE LIMIT REACHED. HALTING PARSER.----
|
||||
{termColors.ENDC}
|
||||
"""
|
||||
)
|
||||
break
|
||||
|
||||
Domain.objects.bulk_create(domains_to_create)
|
||||
DomainInvitation.objects.bulk_create(domain_invitations_to_create)
|
||||
|
||||
self.print_summary_of_findings(
|
||||
domains_to_create,
|
||||
updated_domain_entries,
|
||||
domain_invitations_to_create,
|
||||
skipped_domain_entries,
|
||||
debug_on,
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0038_create_groups_v02"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="transitiondomain",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("ready", "Ready"), ("on hold", "On Hold")],
|
||||
default="ready",
|
||||
help_text="domain status during the transfer",
|
||||
max_length=255,
|
||||
verbose_name="Status",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -4,6 +4,7 @@ import ipaddress
|
|||
import re
|
||||
from datetime import date
|
||||
from string import digits
|
||||
from typing import Optional
|
||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||
|
||||
from django.db import models
|
||||
|
@ -259,7 +260,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""Creates the host object in the registry
|
||||
doesn't add the created host to the domain
|
||||
returns ErrorCode (int)"""
|
||||
logger.info("Creating host")
|
||||
if addrs is not None:
|
||||
addresses = [epp.Ip(addr=addr) for addr in addrs]
|
||||
request = commands.CreateHost(name=host, addrs=addresses)
|
||||
|
@ -456,24 +456,140 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
return [deleteObj], len(deleteStrList)
|
||||
|
||||
@Cache
|
||||
def dnssecdata(self) -> extensions.DNSSECExtension:
|
||||
return self._get_property("dnssecdata")
|
||||
def dnssecdata(self) -> Optional[extensions.DNSSECExtension]:
|
||||
"""
|
||||
Get a complete list of dnssecdata extensions for this domain.
|
||||
|
||||
dnssecdata are provided as a list of DNSSECExtension objects.
|
||||
|
||||
A DNSSECExtension object includes:
|
||||
maxSigLife: Optional[int]
|
||||
dsData: Optional[Sequence[DSData]]
|
||||
keyData: Optional[Sequence[DNSSECKeyData]]
|
||||
|
||||
"""
|
||||
try:
|
||||
return self._get_property("dnssecdata")
|
||||
except Exception as err:
|
||||
# Don't throw error as this is normal for a new domain
|
||||
logger.info("Domain does not have dnssec data defined %s" % err)
|
||||
return None
|
||||
|
||||
def getDnssecdataChanges(
|
||||
self, _dnssecdata: Optional[extensions.DNSSECExtension]
|
||||
) -> tuple[dict, dict]:
|
||||
"""
|
||||
calls self.dnssecdata, it should pull from cache but may result
|
||||
in an epp call
|
||||
returns tuple of 2 values as follows:
|
||||
addExtension: dict
|
||||
remExtension: dict
|
||||
|
||||
addExtension includes all dsData or keyData to be added
|
||||
remExtension includes all dsData or keyData to be removed
|
||||
|
||||
method operates on dsData OR keyData, never a mix of the two;
|
||||
operates based on which is present in _dnssecdata;
|
||||
if neither is present, addExtension will be empty dict, and
|
||||
remExtension will be all existing dnssecdata to be deleted
|
||||
"""
|
||||
|
||||
oldDnssecdata = self.dnssecdata
|
||||
addDnssecdata: dict = {}
|
||||
remDnssecdata: dict = {}
|
||||
|
||||
if _dnssecdata and _dnssecdata.dsData is not None:
|
||||
# initialize addDnssecdata and remDnssecdata for dsData
|
||||
addDnssecdata["dsData"] = _dnssecdata.dsData
|
||||
|
||||
if oldDnssecdata and len(oldDnssecdata.dsData) > 0:
|
||||
# if existing dsData not in new dsData, mark for removal
|
||||
dsDataForRemoval = [
|
||||
dsData
|
||||
for dsData in oldDnssecdata.dsData
|
||||
if dsData not in _dnssecdata.dsData
|
||||
]
|
||||
if len(dsDataForRemoval) > 0:
|
||||
remDnssecdata["dsData"] = dsDataForRemoval
|
||||
|
||||
# if new dsData not in existing dsData, mark for add
|
||||
dsDataForAdd = [
|
||||
dsData
|
||||
for dsData in _dnssecdata.dsData
|
||||
if dsData not in oldDnssecdata.dsData
|
||||
]
|
||||
if len(dsDataForAdd) > 0:
|
||||
addDnssecdata["dsData"] = dsDataForAdd
|
||||
else:
|
||||
addDnssecdata["dsData"] = None
|
||||
|
||||
elif _dnssecdata and _dnssecdata.keyData is not None:
|
||||
# initialize addDnssecdata and remDnssecdata for keyData
|
||||
addDnssecdata["keyData"] = _dnssecdata.keyData
|
||||
|
||||
if oldDnssecdata and len(oldDnssecdata.keyData) > 0:
|
||||
# if existing keyData not in new keyData, mark for removal
|
||||
keyDataForRemoval = [
|
||||
keyData
|
||||
for keyData in oldDnssecdata.keyData
|
||||
if keyData not in _dnssecdata.keyData
|
||||
]
|
||||
if len(keyDataForRemoval) > 0:
|
||||
remDnssecdata["keyData"] = keyDataForRemoval
|
||||
|
||||
# if new keyData not in existing keyData, mark for add
|
||||
keyDataForAdd = [
|
||||
keyData
|
||||
for keyData in _dnssecdata.keyData
|
||||
if keyData not in oldDnssecdata.keyData
|
||||
]
|
||||
if len(keyDataForAdd) > 0:
|
||||
addDnssecdata["keyData"] = keyDataForAdd
|
||||
else:
|
||||
# there are no new dsData or keyData, remove all
|
||||
remDnssecdata["dsData"] = getattr(oldDnssecdata, "dsData", None)
|
||||
remDnssecdata["keyData"] = getattr(oldDnssecdata, "keyData", None)
|
||||
|
||||
return addDnssecdata, remDnssecdata
|
||||
|
||||
@dnssecdata.setter # type: ignore
|
||||
def dnssecdata(self, _dnssecdata: extensions.DNSSECExtension):
|
||||
updateParams = {
|
||||
"maxSigLife": _dnssecdata.get("maxSigLife", None),
|
||||
"dsData": _dnssecdata.get("dsData", None),
|
||||
"keyData": _dnssecdata.get("keyData", None),
|
||||
"remAllDsKeyData": True,
|
||||
def dnssecdata(self, _dnssecdata: Optional[extensions.DNSSECExtension]):
|
||||
_addDnssecdata, _remDnssecdata = self.getDnssecdataChanges(_dnssecdata)
|
||||
addParams = {
|
||||
"maxSigLife": _addDnssecdata.get("maxSigLife", None),
|
||||
"dsData": _addDnssecdata.get("dsData", None),
|
||||
"keyData": _addDnssecdata.get("keyData", None),
|
||||
}
|
||||
request = commands.UpdateDomain(name=self.name)
|
||||
extension = commands.UpdateDomainDNSSECExtension(**updateParams)
|
||||
request.add_extension(extension)
|
||||
remParams = {
|
||||
"maxSigLife": _remDnssecdata.get("maxSigLife", None),
|
||||
"remDsData": _remDnssecdata.get("dsData", None),
|
||||
"remKeyData": _remDnssecdata.get("keyData", None),
|
||||
}
|
||||
addRequest = commands.UpdateDomain(name=self.name)
|
||||
addExtension = commands.UpdateDomainDNSSECExtension(**addParams)
|
||||
addRequest.add_extension(addExtension)
|
||||
remRequest = commands.UpdateDomain(name=self.name)
|
||||
remExtension = commands.UpdateDomainDNSSECExtension(**remParams)
|
||||
remRequest.add_extension(remExtension)
|
||||
try:
|
||||
registry.send(request, cleaned=True)
|
||||
if (
|
||||
"dsData" in _addDnssecdata
|
||||
and _addDnssecdata["dsData"] is not None
|
||||
or "keyData" in _addDnssecdata
|
||||
and _addDnssecdata["keyData"] is not None
|
||||
):
|
||||
registry.send(addRequest, cleaned=True)
|
||||
if (
|
||||
"dsData" in _remDnssecdata
|
||||
and _remDnssecdata["dsData"] is not None
|
||||
or "keyData" in _remDnssecdata
|
||||
and _remDnssecdata["keyData"] is not None
|
||||
):
|
||||
registry.send(remRequest, cleaned=True)
|
||||
except RegistryError as e:
|
||||
logger.error("Error adding DNSSEC, code was %s error was %s" % (e.code, e))
|
||||
logger.error(
|
||||
"Error updating DNSSEC, code was %s error was %s" % (e.code, e)
|
||||
)
|
||||
raise e
|
||||
|
||||
@nameservers.setter # type: ignore
|
||||
|
@ -701,7 +817,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
and errorCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY
|
||||
):
|
||||
# TODO- ticket #433 look here for error handling
|
||||
raise Exception("Unable to add contact to registry")
|
||||
raise RegistryError(code=errorCode)
|
||||
|
||||
# contact doesn't exist on the domain yet
|
||||
logger.info("_set_singleton_contact()-> contact has been added to the registry")
|
||||
|
@ -1128,7 +1244,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
count = 0
|
||||
while not exitEarly and count < 3:
|
||||
try:
|
||||
logger.info("Getting domain info from epp")
|
||||
req = commands.InfoDomain(name=self.name)
|
||||
domainInfoResponse = registry.send(req, cleaned=True)
|
||||
exitEarly = True
|
||||
|
@ -1191,20 +1306,29 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
@transition(
|
||||
field="state", source=[State.READY, State.ON_HOLD], target=State.ON_HOLD
|
||||
)
|
||||
def place_client_hold(self):
|
||||
"""place a clienthold on a domain (no longer should resolve)"""
|
||||
def place_client_hold(self, ignoreEPP=False):
|
||||
"""place a clienthold on a domain (no longer should resolve)
|
||||
ignoreEPP (boolean) - set to true to by-pass EPP (used for transition domains)
|
||||
"""
|
||||
# TODO - ensure all requirements for client hold are made here
|
||||
# (check prohibited statuses)
|
||||
logger.info("clientHold()-> inside clientHold")
|
||||
self._place_client_hold()
|
||||
|
||||
# In order to allow transition domains to by-pass EPP calls,
|
||||
# include this ignoreEPP flag
|
||||
if not ignoreEPP:
|
||||
self._place_client_hold()
|
||||
# TODO -on the client hold ticket any additional error handling here
|
||||
|
||||
@transition(field="state", source=[State.READY, State.ON_HOLD], target=State.READY)
|
||||
def revert_client_hold(self):
|
||||
"""undo a clienthold placed on a domain"""
|
||||
def revert_client_hold(self, ignoreEPP=False):
|
||||
"""undo a clienthold placed on a domain
|
||||
ignoreEPP (boolean) - set to true to by-pass EPP (used for transition domains)
|
||||
"""
|
||||
|
||||
logger.info("clientHold()-> inside clientHold")
|
||||
self._remove_client_hold()
|
||||
if not ignoreEPP:
|
||||
self._remove_client_hold()
|
||||
# TODO -on the client hold ticket any additional error handling here
|
||||
|
||||
@transition(
|
||||
|
@ -1558,74 +1682,84 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""Contact registry for info about a domain."""
|
||||
try:
|
||||
# get info from registry
|
||||
dataResponse = self._get_or_create_domain()
|
||||
data = dataResponse.res_data[0]
|
||||
# extract properties from response
|
||||
# (Ellipsis is used to mean "null")
|
||||
cache = {
|
||||
"auth_info": getattr(data, "auth_info", ...),
|
||||
"_contacts": getattr(data, "contacts", ...),
|
||||
"cr_date": getattr(data, "cr_date", ...),
|
||||
"ex_date": getattr(data, "ex_date", ...),
|
||||
"_hosts": getattr(data, "hosts", ...),
|
||||
"name": getattr(data, "name", ...),
|
||||
"registrant": getattr(data, "registrant", ...),
|
||||
"statuses": getattr(data, "statuses", ...),
|
||||
"tr_date": getattr(data, "tr_date", ...),
|
||||
"up_date": getattr(data, "up_date", ...),
|
||||
}
|
||||
# remove null properties (to distinguish between "a value of None" and null)
|
||||
cleaned = {k: v for k, v in cache.items() if v is not ...}
|
||||
data_response = self._get_or_create_domain()
|
||||
cache = self._extract_data_from_response(data_response)
|
||||
|
||||
# remove null properties (to distinguish between "a value of None" and null)
|
||||
cleaned = self._remove_null_properties(cache)
|
||||
|
||||
# statuses can just be a list no need to keep the epp object
|
||||
if "statuses" in cleaned:
|
||||
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
|
||||
|
||||
# get extensions info, if there is any
|
||||
# DNSSECExtension is one possible extension, make sure to handle
|
||||
# only DNSSECExtension and not other type extensions
|
||||
returned_extensions = dataResponse.extensions
|
||||
cleaned["dnssecdata"] = None
|
||||
for extension in returned_extensions:
|
||||
if isinstance(extension, extensions.DNSSECExtension):
|
||||
cleaned["dnssecdata"] = extension
|
||||
cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions)
|
||||
|
||||
# Capture and store old hosts and contacts from cache if they exist
|
||||
old_cache_hosts = self._cache.get("hosts")
|
||||
old_cache_contacts = self._cache.get("contacts")
|
||||
|
||||
# get contact info, if there are any
|
||||
if (
|
||||
fetch_contacts
|
||||
and "_contacts" in cleaned
|
||||
and isinstance(cleaned["_contacts"], list)
|
||||
and len(cleaned["_contacts"]) > 0
|
||||
):
|
||||
cleaned["contacts"] = self._fetch_contacts(cleaned["_contacts"])
|
||||
# We're only getting contacts, so retain the old
|
||||
# hosts that existed in cache (if they existed)
|
||||
# and pass them along.
|
||||
if fetch_contacts:
|
||||
cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", []))
|
||||
if old_cache_hosts is not None:
|
||||
logger.debug("resetting cleaned['hosts'] to old_cache_hosts")
|
||||
cleaned["hosts"] = old_cache_hosts
|
||||
|
||||
# get nameserver info, if there are any
|
||||
if (
|
||||
fetch_hosts
|
||||
and "_hosts" in cleaned
|
||||
and isinstance(cleaned["_hosts"], list)
|
||||
and len(cleaned["_hosts"])
|
||||
):
|
||||
cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"])
|
||||
# We're only getting hosts, so retain the old
|
||||
# contacts that existed in cache (if they existed)
|
||||
# and pass them along.
|
||||
if fetch_hosts:
|
||||
cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", []))
|
||||
if old_cache_contacts is not None:
|
||||
cleaned["contacts"] = old_cache_contacts
|
||||
# replace the prior cache with new data
|
||||
|
||||
self._cache = cleaned
|
||||
|
||||
except RegistryError as e:
|
||||
logger.error(e)
|
||||
|
||||
def _extract_data_from_response(self, data_response):
|
||||
data = data_response.res_data[0]
|
||||
return {
|
||||
"auth_info": getattr(data, "auth_info", ...),
|
||||
"_contacts": getattr(data, "contacts", ...),
|
||||
"cr_date": getattr(data, "cr_date", ...),
|
||||
"ex_date": getattr(data, "ex_date", ...),
|
||||
"_hosts": getattr(data, "hosts", ...),
|
||||
"name": getattr(data, "name", ...),
|
||||
"registrant": getattr(data, "registrant", ...),
|
||||
"statuses": getattr(data, "statuses", ...),
|
||||
"tr_date": getattr(data, "tr_date", ...),
|
||||
"up_date": getattr(data, "up_date", ...),
|
||||
}
|
||||
|
||||
def _remove_null_properties(self, cache):
|
||||
return {k: v for k, v in cache.items() if v is not ...}
|
||||
|
||||
def _get_dnssec_data(self, response_extensions):
|
||||
# get extensions info, if there is any
|
||||
# DNSSECExtension is one possible extension, make sure to handle
|
||||
# only DNSSECExtension and not other type extensions
|
||||
dnssec_data = None
|
||||
for extension in response_extensions:
|
||||
if isinstance(extension, extensions.DNSSECExtension):
|
||||
dnssec_data = extension
|
||||
return dnssec_data
|
||||
|
||||
def _get_contacts(self, contacts):
|
||||
choices = PublicContact.ContactTypeChoices
|
||||
# We expect that all these fields get populated,
|
||||
# so we can create these early, rather than waiting.
|
||||
cleaned_contacts = {
|
||||
choices.ADMINISTRATIVE: None,
|
||||
choices.SECURITY: None,
|
||||
choices.TECHNICAL: None,
|
||||
}
|
||||
if contacts and isinstance(contacts, list) and len(contacts) > 0:
|
||||
cleaned_contacts = self._fetch_contacts(contacts)
|
||||
return cleaned_contacts
|
||||
|
||||
def _get_hosts(self, hosts):
|
||||
cleaned_hosts = []
|
||||
if hosts and isinstance(hosts, list):
|
||||
cleaned_hosts = self._fetch_hosts(hosts)
|
||||
return cleaned_hosts
|
||||
|
||||
def _get_or_create_public_contact(self, public_contact: PublicContact):
|
||||
"""Tries to find a PublicContact object in our DB.
|
||||
If it can't, it'll create it. Returns PublicContact"""
|
||||
|
|
|
@ -5,7 +5,7 @@ from .utility.time_stamped_model import TimeStampedModel
|
|||
|
||||
class StatusChoices(models.TextChoices):
|
||||
READY = "ready", "Ready"
|
||||
HOLD = "hold", "Hold"
|
||||
ON_HOLD = "on hold", "On Hold"
|
||||
|
||||
|
||||
class TransitionDomain(TimeStampedModel):
|
||||
|
@ -13,6 +13,10 @@ class TransitionDomain(TimeStampedModel):
|
|||
state of a domain upon transition between registry
|
||||
providers"""
|
||||
|
||||
# This is necessary to expose the enum to external
|
||||
# classes that import TransitionDomain
|
||||
StatusChoices = StatusChoices
|
||||
|
||||
username = models.TextField(
|
||||
null=False,
|
||||
blank=False,
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
</div>
|
||||
<br>
|
||||
|
||||
{% url 'domain-nameservers' pk=domain.id as url %}
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% if domain.nameservers|length > 0 %}
|
||||
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
|
||||
{% else %}
|
||||
|
|
20
src/registrar/templates/domain_dns.html
Normal file
20
src/registrar/templates/domain_dns.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block title %}DNS | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
|
||||
<h1>DNS</h1>
|
||||
|
||||
<p>The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to your DNS hosting service and provide us with your name server information.</p>
|
||||
|
||||
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
|
||||
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
<p><a href="{{ url }}">DNS name servers</a></p>
|
||||
|
||||
{% url 'domain-dnssec' pk=domain.id as url %}
|
||||
<p><a href="{{ url }}">DNSSEC</a></p>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
68
src/registrar/templates/domain_dnssec.html
Normal file
68
src/registrar/templates/domain_dnssec.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block title %}DNSSEC | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
|
||||
<h1>DNSSEC</h1>
|
||||
|
||||
<p>DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
|
||||
|
||||
<form class="usa-form usa-form--text-width" method="post">
|
||||
{% csrf_token %}
|
||||
{% if has_dnssec_records %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body">
|
||||
In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone.
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="#toggle-dnssec-alert"
|
||||
class="usa-button"
|
||||
aria-controls="toggle-dnssec-alert"
|
||||
data-open-modal
|
||||
>Disable DNSSEC</a
|
||||
>
|
||||
{% elif dnssec_enabled %}
|
||||
<div id="add-records">
|
||||
<h2> Add DS Records </h2>
|
||||
<p>In order to enable DNSSEC and add Delegation Signer (DS) records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.</p>
|
||||
<p>
|
||||
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button usa-button--outline">Add DS Data</a>
|
||||
<a href="{% url 'domain-dns-dnssec-keydata' pk=domain.id %}" class="usa-button usa-button--outline">Add Key Data</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--unstyled usa-button--cancel"
|
||||
name="cancel_dnssec"
|
||||
id="cancel_dnssec"
|
||||
>Cancel</button>
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="enable-dnssec">
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body">
|
||||
It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
name="enable_dnssec"
|
||||
id="enable_dnssec"
|
||||
>Enable DNSSEC</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-dnssec-alert"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="Your DNSSEC records will be deleted from the registry." modal_button=modal_button|safe %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
123
src/registrar/templates/domain_dsdata.html
Normal file
123
src/registrar/templates/domain_dsdata.html
Normal file
|
@ -0,0 +1,123 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block title %}DS Data | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
{% if domain.dnssecdata is None and not dnssec_ds_confirmed %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
You have no DS Data added. Enable DNSSEC by adding DS Data or return to the DNSSEC page and click 'enable.'
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1>DS Data</h1>
|
||||
|
||||
{% if domain.dnssecdata is not None and domain.dnssecdata.keyData is not None %}
|
||||
<div class="usa-alert usa-alert--warning usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Warning, you cannot add DS Data</h4>
|
||||
<p class="usa-alert__text">
|
||||
You cannot add DS Data because you have already added Key Data. Delete your Key Data records in order to add DS Data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not dnssec_ds_confirmed %}
|
||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||
<p>Enter the values given by your DNS provider for DS Data.</p>
|
||||
<p>Required fields are marked with an asterisk (<abbr
|
||||
title="required"
|
||||
class="usa-hint usa-hint--required"
|
||||
>*</abbr>).</p>
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="usa-button usa-button--unstyled display-block" name="confirm-ds">
|
||||
<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><span class="margin-left-05">Add new record</span>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
<p>Enter the values given by your DNS provider for DS Data.</p>
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
{% for form in formset %}
|
||||
<fieldset class="ds-record">
|
||||
|
||||
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
|
||||
|
||||
<h2 class="margin-top-0">DS Data record {{forloop.counter}}</h2>
|
||||
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.key_tag %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.algorithm %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest_type %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.digest %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row margin-top-1">
|
||||
<div class="grid-col">
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record">
|
||||
<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><span class="margin-left-05">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block margin-bottom-2" id="add-ds-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><span class="margin-left-05">Add new record</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form aria-label="form to undo changes to the DS records">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--outline btn-cancel"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS records to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %} {# domain_content #}
|
110
src/registrar/templates/domain_keydata.html
Normal file
110
src/registrar/templates/domain_keydata.html
Normal file
|
@ -0,0 +1,110 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block title %}Key Data | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
<h1>Key Data</h1>
|
||||
|
||||
{% if domain.dnssecdata is not None and domain.dnssecdata.dsData is not None %}
|
||||
<div class="usa-alert usa-alert--warning usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Warning, you cannot add Key Data</h4>
|
||||
<p class="usa-alert__text">
|
||||
You cannot add Key Data because you have already added DS Data. Delete your DS Data records in order to add Key Data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not dnssec_key_confirmed %}
|
||||
<p>In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.</p>
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
name="confirm-key"
|
||||
>Add DS Key record</button>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
<p>Enter the values given by your DNS provider for DS Key Data.</p>
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
{% for form in formset %}
|
||||
<fieldset class="ds-record">
|
||||
|
||||
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
|
||||
|
||||
<h2 class="margin-top-0">DS Data record {{forloop.counter}}</h2>
|
||||
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.flag %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.protocol %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.algorithm %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.pub_key %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row margin-top-2">
|
||||
<div class="grid-col">
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record">
|
||||
<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><span class="margin-left-05">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block margin-bottom-2" id="add-ds-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><span class="margin-left-05">Add new record</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form aria-label="form to undo changes to the DS records">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--outline btn-cancel"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS records to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %} {# domain_content #}
|
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-form">
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-nameserver-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><span class="margin-left-05">Add another name server</span>
|
||||
|
|
|
@ -13,12 +13,50 @@
|
|||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-nameservers' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
DNS name servers
|
||||
{% url 'domain-dns' pk=domain.id as url %}
|
||||
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}>
|
||||
DNS
|
||||
</a>
|
||||
<ul class="usa-sidenav__sublist">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
Name servers
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path|startswith:url %}class="usa-current"{% endif %}
|
||||
>
|
||||
DNSSEC
|
||||
</a>
|
||||
{% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'data' %}
|
||||
<ul class="usa-sidenav__sublist">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
DS Data
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec-keydata' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
DS Key Data
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
|
|
42
src/registrar/templates/includes/modal.html
Normal file
42
src/registrar/templates/includes/modal.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
{{ modal_heading }}
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p id="modal-1-description">
|
||||
{{ modal_description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ modal_button }}
|
||||
</form>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="/assets/img/sprite.svg#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
|
@ -149,7 +149,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
# see Widget.get_context() on
|
||||
# https://docs.djangoproject.com/en/4.1/ref/forms/widgets
|
||||
widget = field.field.widget.get_context(
|
||||
field.html_name, field.value() or field.initial, field.build_widget_attrs(attrs)
|
||||
field.html_name, field.value(), field.build_widget_attrs(attrs)
|
||||
) # -> {"widget": {"name": ...}}
|
||||
|
||||
context["widget"] = widget["widget"]
|
||||
|
|
|
@ -19,6 +19,13 @@ def startswith(text, starts):
|
|||
return False
|
||||
|
||||
|
||||
@register.filter("endswith")
|
||||
def endswith(text, ends):
|
||||
if isinstance(text, str):
|
||||
return text.endswith(ends)
|
||||
return False
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def public_site_url(url_path):
|
||||
"""Make a full URL for this path at our public site.
|
||||
|
|
|
@ -27,11 +27,14 @@ from registrar.models import (
|
|||
from epplibwrapper import (
|
||||
commands,
|
||||
common,
|
||||
extensions,
|
||||
info,
|
||||
RegistryError,
|
||||
ErrorCode,
|
||||
)
|
||||
|
||||
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -719,6 +722,45 @@ class MockEppLib(TestCase):
|
|||
mockDataHostChange = fakedEppObject(
|
||||
"lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35)
|
||||
)
|
||||
addDsData1 = {
|
||||
"keyTag": 1234,
|
||||
"alg": 3,
|
||||
"digestType": 1,
|
||||
"digest": "ec0bdd990b39feead889f0ba613db4adec0bdd99",
|
||||
}
|
||||
addDsData2 = {
|
||||
"keyTag": 2345,
|
||||
"alg": 3,
|
||||
"digestType": 1,
|
||||
"digest": "ec0bdd990b39feead889f0ba613db4adecb4adec",
|
||||
}
|
||||
keyDataDict = {
|
||||
"flags": 257,
|
||||
"protocol": 3,
|
||||
"alg": 1,
|
||||
"pubKey": "AQPJ////4Q==",
|
||||
}
|
||||
dnssecExtensionWithDsData = extensions.DNSSECExtension(
|
||||
**{
|
||||
"dsData": [
|
||||
common.DSData(**addDsData1) # type: ignore
|
||||
], # type: ignore
|
||||
}
|
||||
)
|
||||
dnssecExtensionWithMultDsData = extensions.DNSSECExtension(
|
||||
**{
|
||||
"dsData": [
|
||||
common.DSData(**addDsData1), # type: ignore
|
||||
common.DSData(**addDsData2), # type: ignore
|
||||
], # type: ignore
|
||||
}
|
||||
)
|
||||
dnssecExtensionWithKeyData = extensions.DNSSECExtension(
|
||||
**{
|
||||
"keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore
|
||||
}
|
||||
)
|
||||
dnssecExtensionRemovingDsData = extensions.DNSSECExtension()
|
||||
|
||||
infoDomainHasIP = fakedEppObject(
|
||||
"nameserverwithip.gov",
|
||||
|
@ -742,58 +784,19 @@ class MockEppLib(TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
def _getattrInfoDomain(self, _request):
|
||||
if getattr(_request, "name", None) == "security.gov":
|
||||
return MagicMock(res_data=[self.infoDomainNoContact])
|
||||
elif getattr(_request, "name", None) == "my-nameserver.gov":
|
||||
if self.mockedSendFunction.call_count == 5:
|
||||
return MagicMock(res_data=[self.infoDomainTwoHosts])
|
||||
else:
|
||||
return MagicMock(res_data=[self.infoDomainNoHost])
|
||||
elif getattr(_request, "name", None) == "nameserverwithip.gov":
|
||||
return MagicMock(res_data=[self.infoDomainHasIP])
|
||||
elif getattr(_request, "name", None) == "namerserversubdomain.gov":
|
||||
return MagicMock(res_data=[self.infoDomainCheckHostIPCombo])
|
||||
elif getattr(_request, "name", None) == "freeman.gov":
|
||||
return MagicMock(res_data=[self.InfoDomainWithContacts])
|
||||
elif getattr(_request, "name", None) == "threenameserversDomain.gov":
|
||||
return MagicMock(res_data=[self.infoDomainThreeHosts])
|
||||
return MagicMock(res_data=[self.mockDataInfoDomain])
|
||||
|
||||
def mockSend(self, _request, cleaned):
|
||||
"""Mocks the registry.send function used inside of domain.py
|
||||
registry is imported from epplibwrapper
|
||||
returns objects that simulate what would be in a epp response
|
||||
but only relevant pieces for tests"""
|
||||
if isinstance(_request, commands.InfoDomain):
|
||||
return self._getattrInfoDomain(_request)
|
||||
|
||||
return self.mockInfoDomainCommands(_request, cleaned)
|
||||
elif isinstance(_request, commands.InfoContact):
|
||||
mocked_result: info.InfoContactResultData
|
||||
|
||||
# For testing contact types
|
||||
match getattr(_request, "id", None):
|
||||
case "securityContact":
|
||||
mocked_result = self.mockSecurityContact
|
||||
case "technicalContact":
|
||||
mocked_result = self.mockTechnicalContact
|
||||
case "adminContact":
|
||||
mocked_result = self.mockAdministrativeContact
|
||||
case "regContact":
|
||||
mocked_result = self.mockRegistrantContact
|
||||
case _:
|
||||
# Default contact return
|
||||
mocked_result = self.mockDataInfoContact
|
||||
|
||||
return MagicMock(res_data=[mocked_result])
|
||||
elif (
|
||||
isinstance(_request, commands.CreateContact)
|
||||
and getattr(_request, "id", None) == "fail"
|
||||
and self.mockedSendFunction.call_count == 3
|
||||
):
|
||||
# use this for when a contact is being updated
|
||||
# sets the second send() to fail
|
||||
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
||||
return self.mockInfoContactCommands(_request, cleaned)
|
||||
elif isinstance(_request, commands.UpdateDomain):
|
||||
return self.mockUpdateDomainCommands(_request, cleaned)
|
||||
elif isinstance(_request, commands.CreateContact):
|
||||
return self.mockCreateContactCommands(_request, cleaned)
|
||||
elif isinstance(_request, commands.CreateHost):
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
|
@ -804,11 +807,6 @@ class MockEppLib(TestCase):
|
|||
res_data=[self.mockDataHostChange],
|
||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
elif isinstance(_request, commands.UpdateDomain):
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
elif isinstance(_request, commands.DeleteHost):
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
|
@ -824,7 +822,93 @@ class MockEppLib(TestCase):
|
|||
raise RegistryError(
|
||||
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION
|
||||
)
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
def mockUpdateDomainCommands(self, _request, cleaned):
|
||||
if getattr(_request, "name", None) == "dnssec-invalid.gov":
|
||||
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
|
||||
def mockInfoDomainCommands(self, _request, cleaned):
|
||||
request_name = getattr(_request, "name", None)
|
||||
|
||||
# Define a dictionary to map request names to data and extension values
|
||||
request_mappings = {
|
||||
"security.gov": (self.infoDomainNoContact, None),
|
||||
"dnssec-dsdata.gov": (
|
||||
self.mockDataInfoDomain,
|
||||
self.dnssecExtensionWithDsData,
|
||||
),
|
||||
"dnssec-multdsdata.gov": (
|
||||
self.mockDataInfoDomain,
|
||||
self.dnssecExtensionWithMultDsData,
|
||||
),
|
||||
"dnssec-keydata.gov": (
|
||||
self.mockDataInfoDomain,
|
||||
self.dnssecExtensionWithKeyData,
|
||||
),
|
||||
"dnssec-none.gov": (self.mockDataInfoDomain, None),
|
||||
"my-nameserver.gov": (
|
||||
self.infoDomainTwoHosts
|
||||
if self.mockedSendFunction.call_count == 5
|
||||
else self.infoDomainNoHost,
|
||||
None,
|
||||
),
|
||||
"nameserverwithip.gov": (self.infoDomainHasIP, None),
|
||||
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
|
||||
"freeman.gov": (self.InfoDomainWithContacts, None),
|
||||
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
|
||||
}
|
||||
|
||||
# Retrieve the corresponding values from the dictionary
|
||||
res_data, extensions = request_mappings.get(
|
||||
request_name, (self.mockDataInfoDomain, None)
|
||||
)
|
||||
|
||||
return MagicMock(
|
||||
res_data=[res_data],
|
||||
extensions=[extensions] if extensions is not None else [],
|
||||
)
|
||||
|
||||
def mockInfoContactCommands(self, _request, cleaned):
|
||||
mocked_result: info.InfoContactResultData
|
||||
|
||||
# For testing contact types
|
||||
match getattr(_request, "id", None):
|
||||
case "securityContact":
|
||||
mocked_result = self.mockSecurityContact
|
||||
case "technicalContact":
|
||||
mocked_result = self.mockTechnicalContact
|
||||
case "adminContact":
|
||||
mocked_result = self.mockAdministrativeContact
|
||||
case "regContact":
|
||||
mocked_result = self.mockRegistrantContact
|
||||
case _:
|
||||
# Default contact return
|
||||
mocked_result = self.mockDataInfoContact
|
||||
|
||||
return MagicMock(res_data=[mocked_result])
|
||||
|
||||
def mockCreateContactCommands(self, _request, cleaned):
|
||||
if (
|
||||
getattr(_request, "id", None) == "fail"
|
||||
and self.mockedSendFunction.call_count == 3
|
||||
):
|
||||
# use this for when a contact is being updated
|
||||
# sets the second send() to fail
|
||||
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
||||
elif getattr(_request, "email", None) == "test@failCreate.gov":
|
||||
# use this for when a contact is being updated
|
||||
# mocks a registry error on creation
|
||||
raise RegistryError(code=None)
|
||||
elif getattr(_request, "email", None) == "test@contactError.gov":
|
||||
# use this for when a contact is being updated
|
||||
# mocks a contact error on creation
|
||||
raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE)
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -3,7 +3,6 @@ Feature being tested: Registry Integration
|
|||
|
||||
This file tests the various ways in which the registrar interacts with the registry.
|
||||
"""
|
||||
from typing import Mapping, Any
|
||||
from django.test import TestCase
|
||||
from django.db.utils import IntegrityError
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
|
@ -1442,15 +1441,27 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
"""Rule: Registrants may modify their secure DNS data"""
|
||||
|
||||
# helper function to create UpdateDomainDNSSECExtention object for verification
|
||||
def createUpdateExtension(self, dnssecdata: extensions.DNSSECExtension):
|
||||
return commands.UpdateDomainDNSSECExtension(
|
||||
maxSigLife=dnssecdata.maxSigLife,
|
||||
dsData=dnssecdata.dsData,
|
||||
keyData=dnssecdata.keyData,
|
||||
remDsData=None,
|
||||
remKeyData=None,
|
||||
remAllDsKeyData=True,
|
||||
)
|
||||
def createUpdateExtension(
|
||||
self, dnssecdata: extensions.DNSSECExtension, remove=False
|
||||
):
|
||||
if not remove:
|
||||
return commands.UpdateDomainDNSSECExtension(
|
||||
maxSigLife=dnssecdata.maxSigLife,
|
||||
dsData=dnssecdata.dsData,
|
||||
keyData=dnssecdata.keyData,
|
||||
remDsData=None,
|
||||
remKeyData=None,
|
||||
remAllDsKeyData=False,
|
||||
)
|
||||
else:
|
||||
return commands.UpdateDomainDNSSECExtension(
|
||||
maxSigLife=dnssecdata.maxSigLife,
|
||||
dsData=None,
|
||||
keyData=None,
|
||||
remDsData=dnssecdata.dsData,
|
||||
remKeyData=dnssecdata.keyData,
|
||||
remAllDsKeyData=False,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
|
@ -1461,37 +1472,6 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
super().setUp()
|
||||
# for the tests, need a domain in the unknown state
|
||||
self.domain, _ = Domain.objects.get_or_create(name="fake.gov")
|
||||
self.addDsData1 = {
|
||||
"keyTag": 1234,
|
||||
"alg": 3,
|
||||
"digestType": 1,
|
||||
"digest": "ec0bdd990b39feead889f0ba613db4adec0bdd99",
|
||||
}
|
||||
self.addDsData2 = {
|
||||
"keyTag": 2345,
|
||||
"alg": 3,
|
||||
"digestType": 1,
|
||||
"digest": "ec0bdd990b39feead889f0ba613db4adecb4adec",
|
||||
}
|
||||
self.keyDataDict = {
|
||||
"flags": 257,
|
||||
"protocol": 3,
|
||||
"alg": 1,
|
||||
"pubKey": "AQPJ////4Q==",
|
||||
}
|
||||
self.dnssecExtensionWithDsData: Mapping[str, Any] = {
|
||||
"dsData": [common.DSData(**self.addDsData1)]
|
||||
}
|
||||
self.dnssecExtensionWithMultDsData: Mapping[str, Any] = {
|
||||
"dsData": [
|
||||
common.DSData(**self.addDsData1),
|
||||
common.DSData(**self.addDsData2),
|
||||
],
|
||||
}
|
||||
self.dnssecExtensionWithKeyData: Mapping[str, Any] = {
|
||||
"maxSigLife": 3215,
|
||||
"keyData": [common.DNSSECKeyData(**self.keyDataDict)],
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
Domain.objects.all().delete()
|
||||
|
@ -1499,51 +1479,62 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
|
||||
def test_user_adds_dnssec_data(self):
|
||||
"""
|
||||
Scenario: Registrant adds DNSSEC data.
|
||||
Scenario: Registrant adds DNSSEC ds data.
|
||||
Verify that both the setter and getter are functioning properly
|
||||
|
||||
This test verifies:
|
||||
1 - setter calls UpdateDomain command
|
||||
2 - setter adds the UpdateDNSSECExtension extension to the command
|
||||
3 - setter causes the getter to call info domain on next get from cache
|
||||
4 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||
1 - setter initially calls InfoDomain command
|
||||
2 - setter then calls UpdateDomain command
|
||||
3 - setter adds the UpdateDNSSECExtension extension to the command
|
||||
4 - setter causes the getter to call info domain on next get from cache
|
||||
5 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||
|
||||
"""
|
||||
|
||||
# make sure to stop any other patcher so there are no conflicts
|
||||
self.mockSendPatch.stop()
|
||||
|
||||
# need to use a separate patcher and side_effect for this test, as
|
||||
# response from InfoDomain must be different for different iterations
|
||||
# of the same command
|
||||
def side_effect(_request, cleaned):
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[
|
||||
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
|
||||
],
|
||||
)
|
||||
if isinstance(_request, commands.InfoDomain):
|
||||
if mocked_send.call_count == 1:
|
||||
return MagicMock(res_data=[self.mockDataInfoDomain])
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[self.dnssecExtensionWithDsData],
|
||||
)
|
||||
else:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
self.domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
|
||||
domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
|
||||
# get the DNS SEC extension added to the UpdateDomain command and
|
||||
# verify that it is properly sent
|
||||
# args[0] is the _request sent to registry
|
||||
args, _ = mocked_send.call_args
|
||||
# assert that the extension matches
|
||||
# assert that the extension on the update matches
|
||||
self.assertEquals(
|
||||
args[0].extensions[0],
|
||||
self.createUpdateExtension(
|
||||
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
|
||||
),
|
||||
self.createUpdateExtension(self.dnssecExtensionWithDsData),
|
||||
)
|
||||
# test that the dnssecdata getter is functioning properly
|
||||
dnssecdata_get = self.domain.dnssecdata
|
||||
dnssecdata_get = domain.dnssecdata
|
||||
mocked_send.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="dnssec-dsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-dsdata.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
|
@ -1553,16 +1544,14 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
),
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-dsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"]
|
||||
)
|
||||
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
|
||||
|
||||
patcher.stop()
|
||||
|
||||
|
@ -1575,48 +1564,52 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
# registry normally sends in this case
|
||||
|
||||
This test verifies:
|
||||
1 - UpdateDomain command called twice
|
||||
2 - setter causes the getter to call info domain on next get from cache
|
||||
3 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||
1 - InfoDomain command is called first
|
||||
2 - UpdateDomain command called on the initial setter
|
||||
3 - setter causes the getter to call info domain on next get from cache
|
||||
4 - UpdateDomain command is not called on second setter (no change)
|
||||
5 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||
|
||||
"""
|
||||
|
||||
# make sure to stop any other patcher so there are no conflicts
|
||||
self.mockSendPatch.stop()
|
||||
|
||||
# need to use a separate patcher and side_effect for this test, as
|
||||
# response from InfoDomain must be different for different iterations
|
||||
# of the same command
|
||||
def side_effect(_request, cleaned):
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[
|
||||
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
|
||||
],
|
||||
)
|
||||
if isinstance(_request, commands.InfoDomain):
|
||||
if mocked_send.call_count == 1:
|
||||
return MagicMock(res_data=[self.mockDataInfoDomain])
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[self.dnssecExtensionWithDsData],
|
||||
)
|
||||
else:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
|
||||
|
||||
# set the dnssecdata once
|
||||
self.domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
# set the dnssecdata again
|
||||
self.domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
# test that the dnssecdata getter is functioning properly
|
||||
dnssecdata_get = self.domain.dnssecdata
|
||||
dnssecdata_get = domain.dnssecdata
|
||||
mocked_send.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
commands.InfoDomain(
|
||||
name="dnssec-dsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-dsdata.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
|
@ -1626,16 +1619,20 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
),
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-dsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="dnssec-dsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"]
|
||||
)
|
||||
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
|
||||
|
||||
patcher.stop()
|
||||
|
||||
|
@ -1652,22 +1649,28 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
|
||||
"""
|
||||
|
||||
# make sure to stop any other patcher so there are no conflicts
|
||||
self.mockSendPatch.stop()
|
||||
|
||||
# need to use a separate patcher and side_effect for this test, as
|
||||
# response from InfoDomain must be different for different iterations
|
||||
# of the same command
|
||||
def side_effect(_request, cleaned):
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[
|
||||
extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData)
|
||||
],
|
||||
)
|
||||
if isinstance(_request, commands.InfoDomain):
|
||||
if mocked_send.call_count == 1:
|
||||
return MagicMock(res_data=[self.mockDataInfoDomain])
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[self.dnssecExtensionWithMultDsData],
|
||||
)
|
||||
else:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
self.domain.dnssecdata = self.dnssecExtensionWithMultDsData
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov")
|
||||
|
||||
domain.dnssecdata = self.dnssecExtensionWithMultDsData
|
||||
# get the DNS SEC extension added to the UpdateDomain command
|
||||
# and verify that it is properly sent
|
||||
# args[0] is the _request sent to registry
|
||||
|
@ -1675,17 +1678,15 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
# assert that the extension matches
|
||||
self.assertEquals(
|
||||
args[0].extensions[0],
|
||||
self.createUpdateExtension(
|
||||
extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData)
|
||||
),
|
||||
self.createUpdateExtension(self.dnssecExtensionWithMultDsData),
|
||||
)
|
||||
# test that the dnssecdata getter is functioning properly
|
||||
dnssecdata_get = self.domain.dnssecdata
|
||||
dnssecdata_get = domain.dnssecdata
|
||||
mocked_send.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-multdsdata.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
|
@ -1695,7 +1696,7 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
),
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-multdsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
|
@ -1703,14 +1704,103 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
)
|
||||
|
||||
self.assertEquals(
|
||||
dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData["dsData"]
|
||||
dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData
|
||||
)
|
||||
|
||||
patcher.stop()
|
||||
|
||||
def test_user_removes_dnssec_data(self):
|
||||
"""
|
||||
Scenario: Registrant removes DNSSEC ds data.
|
||||
Verify that both the setter and getter are functioning properly
|
||||
|
||||
This test verifies:
|
||||
1 - setter initially calls InfoDomain command
|
||||
2 - first setter calls UpdateDomain command
|
||||
3 - second setter calls InfoDomain command again
|
||||
3 - setter then calls UpdateDomain command
|
||||
4 - setter adds the UpdateDNSSECExtension extension to the command with rem
|
||||
|
||||
"""
|
||||
|
||||
# need to use a separate patcher and side_effect for this test, as
|
||||
# response from InfoDomain must be different for different iterations
|
||||
# of the same command
|
||||
def side_effect(_request, cleaned):
|
||||
if isinstance(_request, commands.InfoDomain):
|
||||
if mocked_send.call_count == 1:
|
||||
return MagicMock(res_data=[self.mockDataInfoDomain])
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[self.dnssecExtensionWithDsData],
|
||||
)
|
||||
else:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
|
||||
# dnssecdata_get_initial = domain.dnssecdata # call to force initial mock
|
||||
# domain._invalidate_cache()
|
||||
domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
domain.dnssecdata = self.dnssecExtensionRemovingDsData
|
||||
# get the DNS SEC extension added to the UpdateDomain command and
|
||||
# verify that it is properly sent
|
||||
# args[0] is the _request sent to registry
|
||||
args, _ = mocked_send.call_args
|
||||
# assert that the extension on the update matches
|
||||
self.assertEquals(
|
||||
args[0].extensions[0],
|
||||
self.createUpdateExtension(
|
||||
self.dnssecExtensionWithDsData,
|
||||
remove=True,
|
||||
),
|
||||
)
|
||||
mocked_send.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="dnssec-dsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="dnssec-dsdata.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="dnssec-dsdata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="dnssec-dsdata.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
patcher.stop()
|
||||
|
||||
def test_user_adds_dnssec_keydata(self):
|
||||
"""
|
||||
Scenario: Registrant adds DNSSEC data.
|
||||
Scenario: Registrant adds DNSSEC key data.
|
||||
Verify that both the setter and getter are functioning properly
|
||||
|
||||
This test verifies:
|
||||
|
@ -1721,22 +1811,28 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
|
||||
"""
|
||||
|
||||
# make sure to stop any other patcher so there are no conflicts
|
||||
self.mockSendPatch.stop()
|
||||
|
||||
# need to use a separate patcher and side_effect for this test, as
|
||||
# response from InfoDomain must be different for different iterations
|
||||
# of the same command
|
||||
def side_effect(_request, cleaned):
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[
|
||||
extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData)
|
||||
],
|
||||
)
|
||||
if isinstance(_request, commands.InfoDomain):
|
||||
if mocked_send.call_count == 1:
|
||||
return MagicMock(res_data=[self.mockDataInfoDomain])
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[self.dnssecExtensionWithKeyData],
|
||||
)
|
||||
else:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
self.domain.dnssecdata = self.dnssecExtensionWithKeyData
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov")
|
||||
|
||||
domain.dnssecdata = self.dnssecExtensionWithKeyData
|
||||
# get the DNS SEC extension added to the UpdateDomain command
|
||||
# and verify that it is properly sent
|
||||
# args[0] is the _request sent to registry
|
||||
|
@ -1744,17 +1840,15 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
# assert that the extension matches
|
||||
self.assertEquals(
|
||||
args[0].extensions[0],
|
||||
self.createUpdateExtension(
|
||||
extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData)
|
||||
),
|
||||
self.createUpdateExtension(self.dnssecExtensionWithKeyData),
|
||||
)
|
||||
# test that the dnssecdata getter is functioning properly
|
||||
dnssecdata_get = self.domain.dnssecdata
|
||||
dnssecdata_get = domain.dnssecdata
|
||||
mocked_send.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-keydata.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
|
@ -1764,7 +1858,7 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
),
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="fake.gov",
|
||||
name="dnssec-keydata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
|
@ -1772,7 +1866,7 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
)
|
||||
|
||||
self.assertEquals(
|
||||
dnssecdata_get.keyData, self.dnssecExtensionWithKeyData["keyData"]
|
||||
dnssecdata_get.keyData, self.dnssecExtensionWithKeyData.keyData
|
||||
)
|
||||
|
||||
patcher.stop()
|
||||
|
@ -1784,27 +1878,14 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
Then a user-friendly error message is returned for displaying on the web
|
||||
"""
|
||||
|
||||
# make sure to stop any other patcher so there are no conflicts
|
||||
self.mockSendPatch.stop()
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov")
|
||||
|
||||
def side_effect(_request, cleaned):
|
||||
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
# if RegistryError is raised, view formats user-friendly
|
||||
# error message if error is_client_error, is_session_error, or
|
||||
# is_server_error; so test for those conditions
|
||||
with self.assertRaises(RegistryError) as err:
|
||||
self.domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||
self.assertTrue(
|
||||
err.is_client_error() or err.is_session_error() or err.is_server_error()
|
||||
)
|
||||
|
||||
patcher.stop()
|
||||
|
||||
|
||||
class TestAnalystClientHold(MockEppLib):
|
||||
"""Rule: Analysts may suspend or restore a domain by using client hold"""
|
||||
|
|
|
@ -18,6 +18,7 @@ from registrar.models import (
|
|||
DraftDomain,
|
||||
DomainInvitation,
|
||||
Contact,
|
||||
PublicContact,
|
||||
Website,
|
||||
UserDomainRole,
|
||||
User,
|
||||
|
@ -1094,21 +1095,60 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
|
||||
self.domain_multdsdata, _ = Domain.objects.get_or_create(
|
||||
name="dnssec-multdsdata.gov"
|
||||
)
|
||||
self.domain_keydata, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov")
|
||||
# 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_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_keydata
|
||||
)
|
||||
DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain_dnssec_none
|
||||
)
|
||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_multdsdata,
|
||||
role=UserDomainRole.Roles.ADMIN,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_dnssec_none,
|
||||
role=UserDomainRole.Roles.ADMIN,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.domain_information.delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
if hasattr(self.domain, "contacts"):
|
||||
self.domain.contacts.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
self.domain.delete()
|
||||
self.role.delete()
|
||||
PublicContact.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
except ValueError: # pass if already deleted
|
||||
pass
|
||||
super().tearDown()
|
||||
|
@ -1121,7 +1161,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
"domain",
|
||||
"domain-users",
|
||||
"domain-users-add",
|
||||
"domain-nameservers",
|
||||
"domain-dns-nameservers",
|
||||
"domain-org-name-address",
|
||||
"domain-authorizing-official",
|
||||
"domain-your-contact-information",
|
||||
|
@ -1142,7 +1182,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
"domain",
|
||||
"domain-users",
|
||||
"domain-users-add",
|
||||
"domain-nameservers",
|
||||
"domain-dns-nameservers",
|
||||
"domain-org-name-address",
|
||||
"domain-authorizing-official",
|
||||
"domain-your-contact-information",
|
||||
|
@ -1156,7 +1196,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
||||
class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.app.set_user(self.user.username)
|
||||
|
@ -1166,10 +1206,24 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
# click the "Edit" link
|
||||
detail_page = home_page.click("Manage")
|
||||
detail_page = home_page.click("Manage", index=0)
|
||||
self.assertContains(detail_page, "igorville.gov")
|
||||
self.assertContains(detail_page, "Status")
|
||||
|
||||
def test_domain_overview_blocked_for_ineligible_user(self):
|
||||
"""We could easily duplicate this test for all domain management
|
||||
views, but a single url test should be solid enough since all domain
|
||||
management pages share the same permissions class"""
|
||||
self.user.status = User.RESTRICTED
|
||||
self.user.save()
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
with less_console_noise():
|
||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestDomainUserManagement(TestDomainOverview):
|
||||
def test_domain_user_management(self):
|
||||
response = self.client.get(
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||
|
@ -1328,10 +1382,12 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
home_page = self.app.get(reverse("home"))
|
||||
self.assertContains(home_page, self.domain.name)
|
||||
|
||||
|
||||
class TestDomainNameservers(TestDomainOverview):
|
||||
def test_domain_nameservers(self):
|
||||
"""Can load domain's nameservers page."""
|
||||
page = self.client.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(page, "DNS name servers")
|
||||
|
||||
|
@ -1342,7 +1398,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1352,7 +1408,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id}),
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
|
@ -1365,7 +1421,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
nameservers_page = self.app.get(
|
||||
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
@ -1379,6 +1435,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
# the field.
|
||||
self.assertContains(result, "This field is required", count=2, status_code=200)
|
||||
|
||||
|
||||
class TestDomainAuthorizingOfficial(TestDomainOverview):
|
||||
def test_domain_authorizing_official(self):
|
||||
"""Can load domain's authorizing official page."""
|
||||
page = self.client.get(
|
||||
|
@ -1397,6 +1455,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
)
|
||||
self.assertContains(page, "Testy")
|
||||
|
||||
|
||||
class TestDomainOrganization(TestDomainOverview):
|
||||
def test_domain_org_name_address(self):
|
||||
"""Can load domain's org name and mailing address page."""
|
||||
page = self.client.get(
|
||||
|
@ -1433,6 +1493,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
self.assertContains(success_result_page, "Not igorville")
|
||||
self.assertContains(success_result_page, "Faketown")
|
||||
|
||||
|
||||
class TestDomainContactInformation(TestDomainOverview):
|
||||
def test_domain_your_contact_information(self):
|
||||
"""Can load domain's your contact information page."""
|
||||
page = self.client.get(
|
||||
|
@ -1449,6 +1511,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
)
|
||||
self.assertContains(page, "Testy")
|
||||
|
||||
|
||||
class TestDomainSecurityEmail(TestDomainOverview):
|
||||
def test_domain_security_email_existing_security_contact(self):
|
||||
"""Can load domain's security email page."""
|
||||
self.mockSendPatch = patch("registrar.models.domain.registry.send")
|
||||
|
@ -1514,6 +1578,66 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
success_page, "The security email for this domain has been updated"
|
||||
)
|
||||
|
||||
def test_security_email_form_messages(self):
|
||||
"""
|
||||
Test against the success and error messages that are defined in the view
|
||||
"""
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
|
||||
form_data_registry_error = {
|
||||
"security_email": "test@failCreate.gov",
|
||||
}
|
||||
|
||||
form_data_contact_error = {
|
||||
"security_email": "test@contactError.gov",
|
||||
}
|
||||
|
||||
form_data_success = {
|
||||
"security_email": "test@something.gov",
|
||||
}
|
||||
|
||||
test_cases = [
|
||||
(
|
||||
"RegistryError",
|
||||
form_data_registry_error,
|
||||
"Update failed. Cannot contact the registry.",
|
||||
),
|
||||
("ContactError", form_data_contact_error, "Value entered was wrong."),
|
||||
(
|
||||
"RegistrySuccess",
|
||||
form_data_success,
|
||||
"The security email for this domain has been updated.",
|
||||
),
|
||||
# Add more test cases with different scenarios here
|
||||
]
|
||||
|
||||
for test_name, data, expected_message in test_cases:
|
||||
response = self.client.post(
|
||||
reverse("domain-security-email", kwargs={"pk": self.domain.id}),
|
||||
data=data,
|
||||
follow=True,
|
||||
)
|
||||
|
||||
# Check the response status code, content, or any other relevant assertions
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check if the expected message tag is set
|
||||
if test_name == "RegistryError" or test_name == "ContactError":
|
||||
message_tag = "error"
|
||||
elif test_name == "RegistrySuccess":
|
||||
message_tag = "success"
|
||||
else:
|
||||
# Handle other cases if needed
|
||||
message_tag = "info" # Change to the appropriate default
|
||||
|
||||
# Check the message tag
|
||||
messages = list(response.context["messages"])
|
||||
self.assertEqual(len(messages), 1)
|
||||
message = messages[0]
|
||||
self.assertEqual(message.tags, message_tag)
|
||||
self.assertEqual(message.message, expected_message)
|
||||
|
||||
def test_domain_overview_blocked_for_ineligible_user(self):
|
||||
"""We could easily duplicate this test for all domain management
|
||||
views, but a single url test should be solid enough since all domain
|
||||
|
@ -1527,6 +1651,215 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
|
|||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestDomainDNSSEC(TestDomainOverview):
|
||||
|
||||
"""MockEPPLib is already inherited."""
|
||||
|
||||
def test_dnssec_page_refreshes_enable_button(self):
|
||||
"""DNSSEC overview page loads when domain has no DNSSEC data
|
||||
and shows a 'Enable DNSSEC' button. When button is clicked the template
|
||||
updates. When user navigates away then comes back to the page, the
|
||||
'Enable DNSSEC' button is shown again."""
|
||||
# home_page = self.app.get("/")
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(page, "Enable DNSSEC")
|
||||
|
||||
# Prepare the data for the POST request
|
||||
post_data = {
|
||||
"enable_dnssec": "Enable DNSSEC",
|
||||
}
|
||||
updated_page = self.client.post(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(updated_page.status_code, 200)
|
||||
|
||||
self.assertContains(updated_page, "Add DS Data")
|
||||
self.assertContains(updated_page, "Add Key Data")
|
||||
|
||||
self.app.get("/")
|
||||
|
||||
back_to_page = self.client.get(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(back_to_page, "Enable DNSSEC")
|
||||
|
||||
def test_dnssec_page_loads_with_data_in_domain(self):
|
||||
"""DNSSEC overview page loads when domain has DNSSEC data
|
||||
and the template contains a button to disable DNSSEC."""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id})
|
||||
)
|
||||
self.assertContains(page, "Disable DNSSEC")
|
||||
|
||||
# Prepare the data for the POST request
|
||||
post_data = {
|
||||
"disable_dnssec": "Disable DNSSEC",
|
||||
}
|
||||
updated_page = self.client.post(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(updated_page.status_code, 200)
|
||||
|
||||
self.assertContains(updated_page, "Enable DNSSEC")
|
||||
|
||||
def test_ds_form_loads_with_no_domain_data(self):
|
||||
"""DNSSEC Add DS Data page loads when there is no
|
||||
domain DNSSEC data and shows a button to Add new record"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse(
|
||||
"domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id}
|
||||
)
|
||||
)
|
||||
self.assertContains(page, "You have no DS Data added")
|
||||
self.assertContains(page, "Add new record")
|
||||
|
||||
def test_ds_form_loads_with_ds_data(self):
|
||||
"""DNSSEC Add DS Data page loads when there is
|
||||
domain DNSSEC DS data and shows the data"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})
|
||||
)
|
||||
self.assertContains(page, "DS Data record 1")
|
||||
|
||||
def test_ds_form_loads_with_key_data(self):
|
||||
"""DNSSEC Add DS Data page loads when there is
|
||||
domain DNSSEC KEY data and shows an alert"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_keydata.id})
|
||||
)
|
||||
self.assertContains(page, "Warning, you cannot add DS Data")
|
||||
|
||||
def test_key_form_loads_with_no_domain_data(self):
|
||||
"""DNSSEC Add Key Data page loads when there is no
|
||||
domain DNSSEC data and shows a button to Add DS Key record"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse(
|
||||
"domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dnssec_none.id}
|
||||
)
|
||||
)
|
||||
self.assertContains(page, "Add DS Key record")
|
||||
|
||||
def test_key_form_loads_with_key_data(self):
|
||||
"""DNSSEC Add Key Data page loads when there is
|
||||
domain DNSSEC Key data and shows the data"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
|
||||
)
|
||||
self.assertContains(page, "DS Data record 1")
|
||||
|
||||
def test_key_form_loads_with_ds_data(self):
|
||||
"""DNSSEC Add Key Data page loads when there is
|
||||
domain DNSSEC DS data and shows an alert"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dsdata.id})
|
||||
)
|
||||
self.assertContains(page, "Warning, you cannot add Key Data")
|
||||
|
||||
def test_ds_data_form_submits(self):
|
||||
"""DS Data form submits successfully
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post, response should be a redirect
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
self.assertContains(
|
||||
page, "The DS Data records for this domain have been updated."
|
||||
)
|
||||
|
||||
def test_ds_data_form_invalid(self):
|
||||
"""DS Data form errors with invalid data
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
# get a form error
|
||||
add_data_page.forms[0]["form-0-key_tag"] = ""
|
||||
with less_console_noise(): # swallow logged warning message
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# the field.
|
||||
self.assertContains(result, "Key tag is required", count=2, status_code=200)
|
||||
|
||||
def test_key_data_form_submits(self):
|
||||
"""Key Data form submits successfully
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post, response should be a redirect
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
self.assertContains(
|
||||
page, "The Key Data records for this domain have been updated."
|
||||
)
|
||||
|
||||
def test_key_data_form_invalid(self):
|
||||
"""Key Data form errors with invalid data
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
# get a form error
|
||||
add_data_page.forms[0]["form-0-pub_key"] = ""
|
||||
with less_console_noise(): # swallow logged warning message
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# the field.
|
||||
self.assertContains(result, "Pub key is required", count=2, status_code=200)
|
||||
|
||||
|
||||
class TestApplicationStatus(TestWithUser, WebTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
|
@ -3,7 +3,11 @@ from .domain import (
|
|||
DomainView,
|
||||
DomainAuthorizingOfficialView,
|
||||
DomainOrgNameAddressView,
|
||||
DomainDNSView,
|
||||
DomainNameserversView,
|
||||
DomainDNSSECView,
|
||||
DomainDsDataView,
|
||||
DomainKeyDataView,
|
||||
DomainYourContactInformationView,
|
||||
DomainSecurityEmailView,
|
||||
DomainUsersView,
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.contrib import messages
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import redirect
|
||||
from django.template import RequestContext
|
||||
from django.urls import reverse
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
|
@ -22,6 +23,7 @@ from registrar.models import (
|
|||
UserDomainRole,
|
||||
)
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.models.utility.contact_error import ContactError
|
||||
|
||||
from ..forms import (
|
||||
ContactForm,
|
||||
|
@ -29,7 +31,21 @@ from ..forms import (
|
|||
DomainAddUserForm,
|
||||
DomainSecurityEmailForm,
|
||||
NameserverFormset,
|
||||
DomainDnssecForm,
|
||||
DomainDsdataFormset,
|
||||
DomainDsdataForm,
|
||||
DomainKeydataFormset,
|
||||
DomainKeydataForm,
|
||||
)
|
||||
|
||||
from epplibwrapper import (
|
||||
common,
|
||||
extensions,
|
||||
RegistryError,
|
||||
CANNOT_CONTACT_REGISTRY,
|
||||
GENERIC_ERROR,
|
||||
)
|
||||
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
||||
|
||||
|
@ -37,7 +53,80 @@ from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainView(DomainPermissionView):
|
||||
class DomainBaseView(DomainPermissionView):
|
||||
"""
|
||||
Base View for the Domain. Handles getting and setting the domain
|
||||
in session cache on GETs. Also provides methods for getting
|
||||
and setting the domain in cache
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._get_domain(request)
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def _get_domain(self, request):
|
||||
"""
|
||||
get domain from session cache or from db and set
|
||||
to self.object
|
||||
set session to self for downstream functions to
|
||||
update session cache
|
||||
"""
|
||||
self.session = request.session
|
||||
# domain:private_key is the session key to use for
|
||||
# caching the domain in the session
|
||||
domain_pk = "domain:" + str(self.kwargs.get("pk"))
|
||||
cached_domain = self.session.get(domain_pk)
|
||||
|
||||
if cached_domain:
|
||||
self.object = cached_domain
|
||||
else:
|
||||
self.object = self.get_object()
|
||||
self._update_session_with_domain()
|
||||
|
||||
def _update_session_with_domain(self):
|
||||
"""
|
||||
update domain in the session cache
|
||||
"""
|
||||
domain_pk = "domain:" + str(self.kwargs.get("pk"))
|
||||
self.session[domain_pk] = self.object
|
||||
|
||||
|
||||
class DomainFormBaseView(DomainBaseView, FormMixin):
|
||||
"""
|
||||
Form Base View for the Domain. Handles getting and setting
|
||||
domain in cache when dealing with domain forms. Provides
|
||||
implementations of post, form_valid and form_invalid.
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view.
|
||||
|
||||
This post method harmonizes using DomainBaseView and FormMixin
|
||||
"""
|
||||
self._get_domain(request)
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
# updates session cache with domain
|
||||
self._update_session_with_domain()
|
||||
|
||||
# superclass has the redirect
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
# updates session cache with domain
|
||||
self._update_session_with_domain()
|
||||
|
||||
# superclass has the redirect
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DomainView(DomainBaseView):
|
||||
|
||||
"""Domain detail overview page."""
|
||||
|
||||
|
@ -46,10 +135,10 @@ class DomainView(DomainPermissionView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
default_email = Domain().get_default_security_contact().email
|
||||
default_email = self.object.get_default_security_contact().email
|
||||
context["default_security_email"] = default_email
|
||||
|
||||
security_email = self.get_object().get_security_email()
|
||||
security_email = self.object.get_security_email()
|
||||
if security_email is None or security_email == default_email:
|
||||
context["security_email"] = None
|
||||
return context
|
||||
|
@ -57,7 +146,7 @@ class DomainView(DomainPermissionView):
|
|||
return context
|
||||
|
||||
|
||||
class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
|
||||
class DomainOrgNameAddressView(DomainFormBaseView):
|
||||
"""Organization name and mailing address view"""
|
||||
|
||||
model = Domain
|
||||
|
@ -68,25 +157,13 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
|
|||
def get_form_kwargs(self, *args, **kwargs):
|
||||
"""Add domain_info.organization_name instance to make a bound form."""
|
||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
form_kwargs["instance"] = self.get_object().domain_info
|
||||
form_kwargs["instance"] = self.object.domain_info
|
||||
return form_kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
return reverse("domain-org-name-address", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view.
|
||||
|
||||
This post method harmonizes using DetailView and FormMixin together.
|
||||
"""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, save the organization name and mailing address."""
|
||||
form.save()
|
||||
|
@ -99,8 +176,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
|
||||
|
||||
class DomainAuthorizingOfficialView(DomainFormBaseView):
|
||||
"""Domain authorizing official editing view."""
|
||||
|
||||
model = Domain
|
||||
|
@ -111,25 +187,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
|
|||
def get_form_kwargs(self, *args, **kwargs):
|
||||
"""Add domain_info.authorizing_official instance to make a bound form."""
|
||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
form_kwargs["instance"] = self.get_object().domain_info.authorizing_official
|
||||
form_kwargs["instance"] = self.object.domain_info.authorizing_official
|
||||
return form_kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view.
|
||||
|
||||
This post method harmonizes using DetailView and FormMixin together.
|
||||
"""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, save the authorizing official."""
|
||||
form.save()
|
||||
|
@ -142,8 +206,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DomainNameserversView(DomainPermissionView, FormMixin):
|
||||
class DomainDNSView(DomainBaseView):
|
||||
"""DNS Information View."""
|
||||
|
||||
template_name = "domain_dns.html"
|
||||
|
||||
|
||||
class DomainNameserversView(DomainFormBaseView):
|
||||
"""Domain nameserver editing view."""
|
||||
|
||||
template_name = "domain_nameservers.html"
|
||||
|
@ -151,8 +220,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
|||
|
||||
def get_initial(self):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
domain = self.get_object()
|
||||
nameservers = domain.nameservers
|
||||
nameservers = self.object.nameservers
|
||||
initial_data = []
|
||||
|
||||
if nameservers is not None:
|
||||
|
@ -167,7 +235,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
|||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the nameservers page for the domain."""
|
||||
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
|
||||
return reverse("domain-dns-nameservers", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adjust context from FormMixin for formsets."""
|
||||
|
@ -188,16 +256,6 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
|||
form.fields["server"].required = False
|
||||
return formset
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Formset submission posts to this view."""
|
||||
self.object = self.get_object()
|
||||
formset = self.get_form()
|
||||
|
||||
if formset.is_valid():
|
||||
return self.form_valid(formset)
|
||||
else:
|
||||
return self.form_invalid(formset)
|
||||
|
||||
def form_valid(self, formset):
|
||||
"""The formset is valid, perform something with it."""
|
||||
|
||||
|
@ -210,8 +268,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
|||
except KeyError:
|
||||
# no server information in this field, skip it
|
||||
pass
|
||||
domain = self.get_object()
|
||||
domain.nameservers = nameservers
|
||||
self.object.nameservers = nameservers
|
||||
|
||||
messages.success(
|
||||
self.request, "The name servers for this domain have been updated."
|
||||
|
@ -221,8 +278,293 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
|||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainYourContactInformationView(DomainPermissionView, FormMixin):
|
||||
class DomainDNSSECView(DomainFormBaseView):
|
||||
"""Domain DNSSEC editing view."""
|
||||
|
||||
template_name = "domain_dnssec.html"
|
||||
form_class = DomainDnssecForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
has_dnssec_records = self.object.dnssecdata is not None
|
||||
|
||||
# Create HTML for the modal button
|
||||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button" '
|
||||
'name="disable_dnssec">Disable DNSSEC</button>'
|
||||
)
|
||||
|
||||
context["modal_button"] = modal_button
|
||||
context["has_dnssec_records"] = has_dnssec_records
|
||||
context["dnssec_enabled"] = self.request.session.pop("dnssec_enabled", False)
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the DNSSEC page for the domain."""
|
||||
return reverse("domain-dns-dnssec", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view."""
|
||||
self._get_domain(request)
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
if "disable_dnssec" in request.POST:
|
||||
try:
|
||||
self.object.dnssecdata = {}
|
||||
except RegistryError as err:
|
||||
errmsg = "Error removing existing DNSSEC record(s)."
|
||||
logger.error(errmsg + ": " + err)
|
||||
messages.error(self.request, errmsg)
|
||||
request.session["dnssec_ds_confirmed"] = False
|
||||
request.session["dnssec_key_confirmed"] = False
|
||||
elif "enable_dnssec" in request.POST:
|
||||
request.session["dnssec_enabled"] = True
|
||||
request.session["dnssec_ds_confirmed"] = False
|
||||
request.session["dnssec_key_confirmed"] = False
|
||||
|
||||
return self.form_valid(form)
|
||||
|
||||
|
||||
class DomainDsDataView(DomainFormBaseView):
|
||||
"""Domain DNSSEC ds data editing view."""
|
||||
|
||||
template_name = "domain_dsdata.html"
|
||||
form_class = DomainDsdataFormset
|
||||
form = DomainDsdataForm
|
||||
|
||||
def get_initial(self):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
initial_data = []
|
||||
|
||||
if dnssecdata is not None:
|
||||
if dnssecdata.keyData is not None:
|
||||
# TODO: Throw an error
|
||||
# Note: This is moot if we're
|
||||
# removing key data
|
||||
pass
|
||||
|
||||
if dnssecdata.dsData is not None:
|
||||
# Add existing nameservers as initial data
|
||||
initial_data.extend(
|
||||
{
|
||||
"key_tag": record.keyTag,
|
||||
"algorithm": record.alg,
|
||||
"digest_type": record.digestType,
|
||||
"digest": record.digest,
|
||||
}
|
||||
for record in dnssecdata.dsData
|
||||
)
|
||||
|
||||
# Ensure at least 1 record, filled or empty
|
||||
while len(initial_data) == 0:
|
||||
initial_data.append({})
|
||||
|
||||
return initial_data
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the DS Data page for the domain."""
|
||||
return reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adjust context from FormMixin for formsets."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
# use "formset" instead of "form" for the key
|
||||
context["formset"] = context.pop("form")
|
||||
|
||||
# set the dnssec_ds_confirmed flag in the context for this view
|
||||
# based either on the existence of DS Data in the domain,
|
||||
# or on the flag stored in the session
|
||||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
|
||||
if dnssecdata is not None and dnssecdata.dsData is not None:
|
||||
self.request.session["dnssec_ds_confirmed"] = True
|
||||
|
||||
context["dnssec_ds_confirmed"] = self.request.session.get(
|
||||
"dnssec_ds_confirmed", False
|
||||
)
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Formset submission posts to this view."""
|
||||
self._get_domain(request)
|
||||
formset = self.get_form()
|
||||
|
||||
if "confirm-ds" in request.POST:
|
||||
request.session["dnssec_ds_confirmed"] = True
|
||||
request.session["dnssec_key_confirmed"] = False
|
||||
return super().form_valid(formset)
|
||||
|
||||
if "btn-cancel-click" in request.POST:
|
||||
return redirect("/", {"formset": formset}, RequestContext(request))
|
||||
|
||||
if formset.is_valid():
|
||||
return self.form_valid(formset)
|
||||
else:
|
||||
return self.form_invalid(formset)
|
||||
|
||||
def form_valid(self, formset):
|
||||
"""The formset is valid, perform something with it."""
|
||||
|
||||
# Set the dnssecdata from the formset
|
||||
dnssecdata = extensions.DNSSECExtension()
|
||||
|
||||
for form in formset:
|
||||
try:
|
||||
# if 'delete' not in form.cleaned_data
|
||||
# or form.cleaned_data['delete'] == False:
|
||||
dsrecord = {
|
||||
"keyTag": form.cleaned_data["key_tag"],
|
||||
"alg": int(form.cleaned_data["algorithm"]),
|
||||
"digestType": int(form.cleaned_data["digest_type"]),
|
||||
"digest": form.cleaned_data["digest"],
|
||||
}
|
||||
if dnssecdata.dsData is None:
|
||||
dnssecdata.dsData = []
|
||||
dnssecdata.dsData.append(common.DSData(**dsrecord))
|
||||
except KeyError:
|
||||
# no cleaned_data provided for this form, but passed
|
||||
# as valid; this can happen if form has been added but
|
||||
# not been interacted with; in that case, want to ignore
|
||||
pass
|
||||
try:
|
||||
self.object.dnssecdata = dnssecdata
|
||||
except RegistryError as err:
|
||||
errmsg = "Error updating DNSSEC data in the registry."
|
||||
logger.error(errmsg)
|
||||
logger.error(err)
|
||||
messages.error(self.request, errmsg)
|
||||
return self.form_invalid(formset)
|
||||
else:
|
||||
messages.success(
|
||||
self.request, "The DS Data records for this domain have been updated."
|
||||
)
|
||||
# superclass has the redirect
|
||||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainKeyDataView(DomainFormBaseView):
|
||||
"""Domain DNSSEC key data editing view."""
|
||||
|
||||
template_name = "domain_keydata.html"
|
||||
form_class = DomainKeydataFormset
|
||||
form = DomainKeydataForm
|
||||
|
||||
def get_initial(self):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
initial_data = []
|
||||
|
||||
if dnssecdata is not None:
|
||||
if dnssecdata.dsData is not None:
|
||||
# TODO: Throw an error?
|
||||
# Note: this is moot if we're
|
||||
# removing Key data
|
||||
pass
|
||||
|
||||
if dnssecdata.keyData is not None:
|
||||
# Add existing keydata as initial data
|
||||
initial_data.extend(
|
||||
{
|
||||
"flag": record.flags,
|
||||
"protocol": record.protocol,
|
||||
"algorithm": record.alg,
|
||||
"pub_key": record.pubKey,
|
||||
}
|
||||
for record in dnssecdata.keyData
|
||||
)
|
||||
|
||||
# Ensure at least 1 record, filled or empty
|
||||
while len(initial_data) == 0:
|
||||
initial_data.append({})
|
||||
|
||||
return initial_data
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the Key Data page for the domain."""
|
||||
return reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adjust context from FormMixin for formsets."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
# use "formset" instead of "form" for the key
|
||||
context["formset"] = context.pop("form")
|
||||
|
||||
# set the dnssec_key_confirmed flag in the context for this view
|
||||
# based either on the existence of Key Data in the domain,
|
||||
# or on the flag stored in the session
|
||||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
|
||||
if dnssecdata is not None and dnssecdata.keyData is not None:
|
||||
self.request.session["dnssec_key_confirmed"] = True
|
||||
|
||||
context["dnssec_key_confirmed"] = self.request.session.get(
|
||||
"dnssec_key_confirmed", False
|
||||
)
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Formset submission posts to this view."""
|
||||
self._get_domain(request)
|
||||
formset = self.get_form()
|
||||
|
||||
if "confirm-key" in request.POST:
|
||||
request.session["dnssec_key_confirmed"] = True
|
||||
request.session["dnssec_ds_confirmed"] = False
|
||||
self.object.save()
|
||||
return super().form_valid(formset)
|
||||
|
||||
if "btn-cancel-click" in request.POST:
|
||||
return redirect("/", {"formset": formset}, RequestContext(request))
|
||||
|
||||
if formset.is_valid():
|
||||
return self.form_valid(formset)
|
||||
else:
|
||||
return self.form_invalid(formset)
|
||||
|
||||
def form_valid(self, formset):
|
||||
"""The formset is valid, perform something with it."""
|
||||
|
||||
# Set the nameservers from the formset
|
||||
dnssecdata = extensions.DNSSECExtension()
|
||||
|
||||
for form in formset:
|
||||
try:
|
||||
# if 'delete' not in form.cleaned_data
|
||||
# or form.cleaned_data['delete'] == False:
|
||||
keyrecord = {
|
||||
"flags": int(form.cleaned_data["flag"]),
|
||||
"protocol": int(form.cleaned_data["protocol"]),
|
||||
"alg": int(form.cleaned_data["algorithm"]),
|
||||
"pubKey": form.cleaned_data["pub_key"],
|
||||
}
|
||||
if dnssecdata.keyData is None:
|
||||
dnssecdata.keyData = []
|
||||
dnssecdata.keyData.append(common.DNSSECKeyData(**keyrecord))
|
||||
except KeyError:
|
||||
# no server information in this field, skip it
|
||||
pass
|
||||
try:
|
||||
self.object.dnssecdata = dnssecdata
|
||||
except RegistryError as err:
|
||||
errmsg = "Error updating DNSSEC data in the registry."
|
||||
logger.error(errmsg)
|
||||
logger.error(err)
|
||||
messages.error(self.request, errmsg)
|
||||
return self.form_invalid(formset)
|
||||
else:
|
||||
messages.success(
|
||||
self.request, "The Key Data records for this domain have been updated."
|
||||
)
|
||||
# superclass has the redirect
|
||||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainYourContactInformationView(DomainFormBaseView):
|
||||
"""Domain your contact information editing view."""
|
||||
|
||||
template_name = "domain_your_contact_information.html"
|
||||
|
@ -238,16 +580,6 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
|
|||
"""Redirect to the your contact information for the domain."""
|
||||
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
# there is a valid email address in the form
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, call setter in model."""
|
||||
|
||||
|
@ -262,8 +594,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
||||
|
||||
class DomainSecurityEmailView(DomainFormBaseView):
|
||||
"""Domain security email editing view."""
|
||||
|
||||
template_name = "domain_security_email.html"
|
||||
|
@ -271,9 +602,8 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
|||
|
||||
def get_initial(self):
|
||||
"""The initial value for the form."""
|
||||
domain = self.get_object()
|
||||
initial = super().get_initial()
|
||||
security_contact = domain.security_contact
|
||||
security_contact = self.object.security_contact
|
||||
if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov":
|
||||
initial["security_email"] = None
|
||||
return initial
|
||||
|
@ -284,16 +614,6 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
|||
"""Redirect to the security email page for the domain."""
|
||||
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
# there is a valid email address in the form
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, call setter in model."""
|
||||
|
||||
|
@ -304,35 +624,44 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
|||
if new_email is None or new_email.strip() == "":
|
||||
new_email = PublicContact.get_default_security().email
|
||||
|
||||
domain = self.get_object()
|
||||
contact = domain.security_contact
|
||||
contact = self.object.security_contact
|
||||
|
||||
# If no default is created for security_contact,
|
||||
# then we cannot connect to the registry.
|
||||
if contact is None:
|
||||
messages.error(self.request, "Update failed. Cannot contact the registry.")
|
||||
messages.error(self.request, CANNOT_CONTACT_REGISTRY)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
contact.email = new_email
|
||||
contact.save()
|
||||
|
||||
messages.success(
|
||||
self.request, "The security email for this domain has been updated."
|
||||
)
|
||||
try:
|
||||
contact.save()
|
||||
except RegistryError as Err:
|
||||
if Err.is_connection_error():
|
||||
messages.error(self.request, CANNOT_CONTACT_REGISTRY)
|
||||
logger.error(f"Registry connection error: {Err}")
|
||||
else:
|
||||
messages.error(self.request, GENERIC_ERROR)
|
||||
logger.error(f"Registry error: {Err}")
|
||||
except ContactError as Err:
|
||||
messages.error(self.request, GENERIC_ERROR)
|
||||
logger.error(f"Generic registry error: {Err}")
|
||||
else:
|
||||
messages.success(
|
||||
self.request, "The security email for this domain has been updated."
|
||||
)
|
||||
|
||||
# superclass has the redirect
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
class DomainUsersView(DomainPermissionView):
|
||||
|
||||
class DomainUsersView(DomainBaseView):
|
||||
"""User management page in the domain details."""
|
||||
|
||||
template_name = "domain_users.html"
|
||||
|
||||
|
||||
class DomainAddUserView(DomainPermissionView, FormMixin):
|
||||
|
||||
class DomainAddUserView(DomainFormBaseView):
|
||||
"""Inside of a domain's user management, a form for adding users.
|
||||
|
||||
Multiple inheritance is used here for permissions, form handling, and
|
||||
|
@ -345,15 +674,6 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
|
|||
def get_success_url(self):
|
||||
return reverse("domain-users", kwargs={"pk": self.object.pk})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
# there is a valid email address in the form
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def _domain_abs_url(self):
|
||||
"""Get an absolute URL for this domain."""
|
||||
return self.request.build_absolute_uri(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue