mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-28 13:36:30 +02:00
merge main and fix migrations
This commit is contained in:
commit
7cd2c6fef3
36 changed files with 2389 additions and 1622 deletions
|
@ -5,6 +5,11 @@ from django import forms
|
||||||
from django.db.models import Value, CharField, Q
|
from django.db.models import Value, CharField, Q
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
from registrar.utility.admin_helpers import (
|
||||||
|
get_action_needed_reason_default_email,
|
||||||
|
get_rejection_reason_default_email,
|
||||||
|
get_field_links_as_list,
|
||||||
|
)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django_fsm import get_available_FIELD_transitions, FSMField
|
from django_fsm import get_available_FIELD_transitions, FSMField
|
||||||
|
@ -20,11 +25,6 @@ from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from waffle.admin import FlagAdmin
|
from waffle.admin import FlagAdmin
|
||||||
from waffle.models import Sample, Switch
|
from waffle.models import Sample, Switch
|
||||||
from registrar.utility.admin_helpers import (
|
|
||||||
get_all_action_needed_reason_emails,
|
|
||||||
get_action_needed_reason_default_email,
|
|
||||||
get_field_links_as_list,
|
|
||||||
)
|
|
||||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
|
@ -237,6 +237,7 @@ class DomainRequestAdminForm(forms.ModelForm):
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
"action_needed_reason_email": "Email",
|
"action_needed_reason_email": "Email",
|
||||||
|
"rejection_reason_email": "Email",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -1750,6 +1751,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"status_history",
|
"status_history",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
|
"rejection_reason_email",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
"action_needed_reason_email",
|
"action_needed_reason_email",
|
||||||
"investigator",
|
"investigator",
|
||||||
|
@ -1905,25 +1907,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
# Get the original domain request from the database.
|
# Get the original domain request from the database.
|
||||||
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
||||||
|
|
||||||
# == Handle action_needed_reason == #
|
# == Handle action needed and rejected emails == #
|
||||||
|
# Edge case: this logic is handled by javascript, so contexts outside that must be handled
|
||||||
reason_changed = obj.action_needed_reason != original_obj.action_needed_reason
|
obj = self._handle_custom_emails(obj)
|
||||||
if reason_changed:
|
|
||||||
# Track the fact that we sent out an email
|
|
||||||
request.session["action_needed_email_sent"] = True
|
|
||||||
|
|
||||||
# Set the action_needed_reason_email to the default if nothing exists.
|
|
||||||
# Since this check occurs after save, if the user enters a value then we won't update.
|
|
||||||
|
|
||||||
default_email = get_action_needed_reason_default_email(request, obj, obj.action_needed_reason)
|
|
||||||
if obj.action_needed_reason_email:
|
|
||||||
emails = get_all_action_needed_reason_emails(request, obj)
|
|
||||||
is_custom_email = obj.action_needed_reason_email not in emails.values()
|
|
||||||
if not is_custom_email:
|
|
||||||
obj.action_needed_reason_email = default_email
|
|
||||||
else:
|
|
||||||
obj.action_needed_reason_email = default_email
|
|
||||||
|
|
||||||
|
# == Handle allowed emails == #
|
||||||
if obj.status in DomainRequest.get_statuses_that_send_emails() and not settings.IS_PRODUCTION:
|
if obj.status in DomainRequest.get_statuses_that_send_emails() and not settings.IS_PRODUCTION:
|
||||||
self._check_for_valid_email(request, obj)
|
self._check_for_valid_email(request, obj)
|
||||||
|
|
||||||
|
@ -1939,6 +1927,15 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
if should_save:
|
if should_save:
|
||||||
return super().save_model(request, obj, form, change)
|
return super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def _handle_custom_emails(self, obj):
|
||||||
|
if obj.status == DomainRequest.DomainRequestStatus.ACTION_NEEDED:
|
||||||
|
if obj.action_needed_reason and not obj.action_needed_reason_email:
|
||||||
|
obj.action_needed_reason_email = get_action_needed_reason_default_email(obj, obj.action_needed_reason)
|
||||||
|
elif obj.status == DomainRequest.DomainRequestStatus.REJECTED:
|
||||||
|
if obj.rejection_reason and not obj.rejection_reason_email:
|
||||||
|
obj.rejection_reason_email = get_rejection_reason_default_email(obj, obj.rejection_reason)
|
||||||
|
return obj
|
||||||
|
|
||||||
def _check_for_valid_email(self, request, obj):
|
def _check_for_valid_email(self, request, obj):
|
||||||
"""Certain emails are whitelisted in non-production environments,
|
"""Certain emails are whitelisted in non-production environments,
|
||||||
so we should display that information using this function.
|
so we should display that information using this function.
|
||||||
|
@ -1976,18 +1973,30 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
# If the status is not mapped properly, saving could cause
|
# If the status is not mapped properly, saving could cause
|
||||||
# weird issues down the line. Instead, we should block this.
|
# weird issues down the line. Instead, we should block this.
|
||||||
|
# NEEDS A UNIT TEST
|
||||||
should_proceed = False
|
should_proceed = False
|
||||||
return should_proceed
|
return (obj, should_proceed)
|
||||||
|
|
||||||
request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
obj_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
if request_is_not_approved and not obj.domain_is_not_active():
|
if obj_is_not_approved and not obj.domain_is_not_active():
|
||||||
# If an admin tried to set an approved domain request to
|
# REDUNDANT CHECK / ERROR SCREEN AVOIDANCE:
|
||||||
# another status and the related domain is already
|
# This action (moving a request from approved to
|
||||||
# active, shortcut the action and throw a friendly
|
# another status) when the domain is already active (READY),
|
||||||
# error message. This action would still not go through
|
# would still not go through even without this check as the rules are
|
||||||
# shortcut or not as the rules are duplicated on the model,
|
# duplicated in the model and the error is raised from the model.
|
||||||
# but the error would be an ugly Django error screen.
|
# This avoids an ugly Django error screen.
|
||||||
error_message = "This action is not permitted. The domain is already active."
|
error_message = "This action is not permitted. The domain is already active."
|
||||||
|
elif (
|
||||||
|
original_obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
and obj.status == models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
and original_obj.requested_domain is not None
|
||||||
|
and Domain.objects.filter(name=original_obj.requested_domain.name).exists()
|
||||||
|
):
|
||||||
|
# REDUNDANT CHECK:
|
||||||
|
# This action (approving a request when the domain exists)
|
||||||
|
# would still not go through even without this check as the rules are
|
||||||
|
# duplicated in the model and the error is raised from the model.
|
||||||
|
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.APPROVE_DOMAIN_IN_USE)
|
||||||
elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason:
|
elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason:
|
||||||
# This condition should never be triggered.
|
# This condition should never be triggered.
|
||||||
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
||||||
|
|
|
@ -344,69 +344,6 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
|
||||||
* status select and to show/hide the rejection reason
|
|
||||||
*/
|
|
||||||
(function (){
|
|
||||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
|
||||||
// This is the "action needed reason" field
|
|
||||||
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
|
||||||
// This is the "Email" field
|
|
||||||
let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email')
|
|
||||||
|
|
||||||
if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) {
|
|
||||||
let statusSelect = document.getElementById('id_status')
|
|
||||||
let isRejected = statusSelect.value == "rejected"
|
|
||||||
let isActionNeeded = statusSelect.value == "action needed"
|
|
||||||
|
|
||||||
// Initial handling of rejectionReasonFormGroup display
|
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
|
||||||
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
|
||||||
|
|
||||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
|
||||||
statusSelect.addEventListener('change', function() {
|
|
||||||
// Show the rejection reason field if the status is rejected.
|
|
||||||
// Then track if its shown or hidden in our session cache.
|
|
||||||
isRejected = statusSelect.value == "rejected"
|
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
|
||||||
addOrRemoveSessionBoolean("showRejectionReason", add=isRejected)
|
|
||||||
|
|
||||||
isActionNeeded = statusSelect.value == "action needed"
|
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
|
||||||
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
|
||||||
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
|
||||||
|
|
||||||
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
|
||||||
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
|
|
||||||
// accurately for this edge case, we use cache and test for the back/forward navigation.
|
|
||||||
const observer = new PerformanceObserver((list) => {
|
|
||||||
list.getEntries().forEach((entry) => {
|
|
||||||
if (entry.type === "back_forward") {
|
|
||||||
let showRejectionReason = sessionStorage.getItem("showRejectionReason") !== null
|
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=showRejectionReason)
|
|
||||||
|
|
||||||
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
|
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
|
|
||||||
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
observer.observe({ type: "navigation" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds or removes the display-none class to object depending on the value of boolean show
|
|
||||||
function showOrHideObject(object, show){
|
|
||||||
if (show){
|
|
||||||
object.classList.remove("display-none");
|
|
||||||
}else {
|
|
||||||
object.classList.add("display-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
/** An IIFE for toggling the submit bar on domain request forms
|
/** An IIFE for toggling the submit bar on domain request forms
|
||||||
*/
|
*/
|
||||||
|
@ -501,82 +438,110 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
class CustomizableEmailBase {
|
||||||
* This shows the auto generated email on action needed reason.
|
|
||||||
*/
|
/**
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
* @param {Object} config - must contain the following:
|
||||||
const dropdown = document.getElementById("id_action_needed_reason");
|
* @property {HTMLElement} dropdown - The dropdown element.
|
||||||
const textarea = document.getElementById("id_action_needed_reason_email")
|
* @property {HTMLElement} textarea - The textarea element.
|
||||||
const domainRequestId = dropdown ? document.getElementById("domain_request_id").value : null
|
* @property {HTMLElement} lastSentEmailContent - The last sent email content element.
|
||||||
const textareaPlaceholder = document.querySelector(".field-action_needed_reason_email__placeholder");
|
* @property {HTMLElement} textAreaFormGroup - The form group for the textarea.
|
||||||
const directEditButton = document.querySelector('.field-action_needed_reason_email__edit');
|
* @property {HTMLElement} dropdownFormGroup - The form group for the dropdown.
|
||||||
const modalTrigger = document.querySelector('.field-action_needed_reason_email__modal-trigger');
|
* @property {HTMLElement} modalConfirm - The confirm button in the modal.
|
||||||
const modalConfirm = document.getElementById('confirm-edit-email');
|
* @property {string} apiUrl - The API URL for fetching email content.
|
||||||
const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]');
|
* @property {string} statusToCheck - The status to check against. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||||
let lastSentEmailContent = document.getElementById("last-sent-email-content");
|
* @property {string} sessionVariableName - The session variable name. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||||
const initialDropdownValue = dropdown ? dropdown.value : null;
|
* @property {string} apiErrorMessage - The error message that the ajax call returns.
|
||||||
const initialEmailValue = textarea.value;
|
*/
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.dropdown = config.dropdown;
|
||||||
|
this.textarea = config.textarea;
|
||||||
|
this.lastSentEmailContent = config.lastSentEmailContent;
|
||||||
|
this.apiUrl = config.apiUrl;
|
||||||
|
this.apiErrorMessage = config.apiErrorMessage;
|
||||||
|
this.modalConfirm = config.modalConfirm;
|
||||||
|
|
||||||
|
// These fields are hidden/shown on pageload depending on the current status
|
||||||
|
this.textAreaFormGroup = config.textAreaFormGroup;
|
||||||
|
this.dropdownFormGroup = config.dropdownFormGroup;
|
||||||
|
this.statusToCheck = config.statusToCheck;
|
||||||
|
this.sessionVariableName = config.sessionVariableName;
|
||||||
|
|
||||||
|
// Non-configurable variables
|
||||||
|
this.statusSelect = document.getElementById("id_status");
|
||||||
|
this.domainRequestId = this.dropdown ? document.getElementById("domain_request_id").value : null
|
||||||
|
this.initialDropdownValue = this.dropdown ? this.dropdown.value : null;
|
||||||
|
this.initialEmailValue = this.textarea ? this.textarea.value : null;
|
||||||
|
|
||||||
|
// Find other fields near the textarea
|
||||||
|
const parentDiv = this.textarea ? this.textarea.closest(".flex-container") : null;
|
||||||
|
this.directEditButton = parentDiv ? parentDiv.querySelector(".edit-email-button") : null;
|
||||||
|
this.modalTrigger = parentDiv ? parentDiv.querySelector(".edit-button-modal-trigger") : null;
|
||||||
|
|
||||||
|
this.textareaPlaceholder = parentDiv ? parentDiv.querySelector(".custom-email-placeholder") : null;
|
||||||
|
this.formLabel = this.textarea ? document.querySelector(`label[for="${this.textarea.id}"]`) : null;
|
||||||
|
|
||||||
|
this.isEmailAlreadySentConst;
|
||||||
|
if (this.lastSentEmailContent && this.textarea) {
|
||||||
|
this.isEmailAlreadySentConst = this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
// We will use the const to control the modal
|
|
||||||
let isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
|
||||||
// We will use the function to control the label and help
|
|
||||||
function isEmailAlreadySent() {
|
|
||||||
return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dropdown || !textarea || !domainRequestId || !formLabel || !modalConfirm) return;
|
// Handle showing/hiding the related fields on page load.
|
||||||
const apiUrl = document.getElementById("get-action-needed-email-for-user-json").value;
|
initializeFormGroups() {
|
||||||
|
let isStatus = this.statusSelect.value == this.statusToCheck;
|
||||||
|
|
||||||
function updateUserInterface(reason) {
|
// Initial handling of these groups.
|
||||||
if (!reason) {
|
this.updateFormGroupVisibility(isStatus);
|
||||||
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
|
|
||||||
formLabel.innerHTML = "Email:";
|
|
||||||
textareaPlaceholder.innerHTML = "Select an action needed reason to see email";
|
|
||||||
showElement(textareaPlaceholder);
|
|
||||||
hideElement(directEditButton);
|
|
||||||
hideElement(modalTrigger);
|
|
||||||
hideElement(textarea);
|
|
||||||
} else if (reason === 'other') {
|
|
||||||
// 'Other' selected, we will set the label to "Email", show the "No email will be sent" placeholder, hide the trigger, textarea, hide the help text
|
|
||||||
formLabel.innerHTML = "Email:";
|
|
||||||
textareaPlaceholder.innerHTML = "No email will be sent";
|
|
||||||
showElement(textareaPlaceholder);
|
|
||||||
hideElement(directEditButton);
|
|
||||||
hideElement(modalTrigger);
|
|
||||||
hideElement(textarea);
|
|
||||||
} else {
|
|
||||||
// A triggering selection is selected, all hands on board:
|
|
||||||
textarea.setAttribute('readonly', true);
|
|
||||||
showElement(textarea);
|
|
||||||
hideElement(textareaPlaceholder);
|
|
||||||
|
|
||||||
if (isEmailAlreadySentConst) {
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
hideElement(directEditButton);
|
this.statusSelect.addEventListener('change', () => {
|
||||||
showElement(modalTrigger);
|
// Show the action needed field if the status is what we expect.
|
||||||
} else {
|
// Then track if its shown or hidden in our session cache.
|
||||||
showElement(directEditButton);
|
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||||
hideElement(modalTrigger);
|
this.updateFormGroupVisibility(isStatus);
|
||||||
}
|
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
||||||
if (isEmailAlreadySent()) {
|
});
|
||||||
formLabel.innerHTML = "Email sent to creator:";
|
|
||||||
} else {
|
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
||||||
formLabel.innerHTML = "Email:";
|
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||||
}
|
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
|
||||||
|
// accurately for this edge case, we use cache and test for the back/forward navigation.
|
||||||
|
const observer = new PerformanceObserver((list) => {
|
||||||
|
list.getEntries().forEach((entry) => {
|
||||||
|
if (entry.type === "back_forward") {
|
||||||
|
let showTextAreaFormGroup = sessionStorage.getItem(this.sessionVariableName) !== null;
|
||||||
|
this.updateFormGroupVisibility(showTextAreaFormGroup);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe({ type: "navigation" });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFormGroupVisibility(showFormGroups) {
|
||||||
|
if (showFormGroups) {
|
||||||
|
showElement(this.textAreaFormGroup);
|
||||||
|
showElement(this.dropdownFormGroup);
|
||||||
|
}else {
|
||||||
|
hideElement(this.textAreaFormGroup);
|
||||||
|
hideElement(this.dropdownFormGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize UI
|
initializeDropdown() {
|
||||||
updateUserInterface(dropdown.value);
|
this.dropdown.addEventListener("change", () => {
|
||||||
|
let reason = this.dropdown.value;
|
||||||
dropdown.addEventListener("change", function() {
|
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||||
const reason = dropdown.value;
|
let searchParams = new URLSearchParams(
|
||||||
// Update the UI
|
{
|
||||||
updateUserInterface(reason);
|
"reason": reason,
|
||||||
if (reason && reason !== "other") {
|
"domain_request_id": this.domainRequestId,
|
||||||
// If it's not the initial value
|
}
|
||||||
if (initialDropdownValue !== dropdown.value || initialEmailValue !== textarea.value) {
|
);
|
||||||
// Replace the email content
|
// Replace the email content
|
||||||
fetch(`${apiUrl}?reason=${reason}&domain_request_id=${domainRequestId}`)
|
fetch(`${this.apiUrl}?${searchParams.toString()}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.json().then(data => data);
|
return response.json().then(data => data);
|
||||||
})
|
})
|
||||||
|
@ -584,30 +549,213 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.error("Error in AJAX call: " + data.error);
|
console.error("Error in AJAX call: " + data.error);
|
||||||
}else {
|
}else {
|
||||||
textarea.value = data.action_needed_email;
|
this.textarea.value = data.email;
|
||||||
}
|
}
|
||||||
updateUserInterface(reason);
|
this.updateUserInterface(reason);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error action needed email: ", error)
|
console.error(this.apiErrorMessage, error)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeModalConfirm() {
|
||||||
|
this.modalConfirm.addEventListener("click", () => {
|
||||||
|
this.textarea.removeAttribute('readonly');
|
||||||
|
this.textarea.focus();
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDirectEditButton() {
|
||||||
|
this.directEditButton.addEventListener("click", () => {
|
||||||
|
this.textarea.removeAttribute('readonly');
|
||||||
|
this.textarea.focus();
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmailAlreadySent() {
|
||||||
|
return this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserInterface(reason=this.dropdown.value, excluded_reasons=["other"]) {
|
||||||
|
if (!reason) {
|
||||||
|
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
|
||||||
|
this.showPlaceholderNoReason();
|
||||||
|
} else if (excluded_reasons.includes(reason)) {
|
||||||
|
// 'Other' selected, we will set the label to "Email", show the "No email will be sent" placeholder, hide the trigger, textarea, hide the help text
|
||||||
|
this.showPlaceholderOtherReason();
|
||||||
|
} else {
|
||||||
|
this.showReadonlyTextarea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function that makes overriding the readonly textarea easy
|
||||||
|
showReadonlyTextarea() {
|
||||||
|
// A triggering selection is selected, all hands on board:
|
||||||
|
this.textarea.setAttribute('readonly', true);
|
||||||
|
showElement(this.textarea);
|
||||||
|
hideElement(this.textareaPlaceholder);
|
||||||
|
|
||||||
|
if (this.isEmailAlreadySentConst) {
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
showElement(this.modalTrigger);
|
||||||
|
} else {
|
||||||
|
showElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
if (this.isEmailAlreadySent()) {
|
||||||
|
this.formLabel.innerHTML = "Email sent to creator:";
|
||||||
|
} else {
|
||||||
|
this.formLabel.innerHTML = "Email:";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
modalConfirm.addEventListener("click", () => {
|
// Helper function that makes overriding the placeholder reason easy
|
||||||
textarea.removeAttribute('readonly');
|
showPlaceholderNoReason() {
|
||||||
textarea.focus();
|
this.showPlaceholder("Email:", "Select a reason to see email");
|
||||||
hideElement(directEditButton);
|
}
|
||||||
hideElement(modalTrigger);
|
|
||||||
});
|
// Helper function that makes overriding the placeholder reason easy
|
||||||
directEditButton.addEventListener("click", () => {
|
showPlaceholderOtherReason() {
|
||||||
textarea.removeAttribute('readonly');
|
this.showPlaceholder("Email:", "No email will be sent");
|
||||||
textarea.focus();
|
}
|
||||||
hideElement(directEditButton);
|
|
||||||
hideElement(modalTrigger);
|
showPlaceholder(formLabelText, placeholderText) {
|
||||||
});
|
this.formLabel.innerHTML = formLabelText;
|
||||||
|
this.textareaPlaceholder.innerHTML = placeholderText;
|
||||||
|
showElement(this.textareaPlaceholder);
|
||||||
|
hideElement(this.directEditButton);
|
||||||
|
hideElement(this.modalTrigger);
|
||||||
|
hideElement(this.textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class customActionNeededEmail extends CustomizableEmailBase {
|
||||||
|
constructor() {
|
||||||
|
const emailConfig = {
|
||||||
|
dropdown: document.getElementById("id_action_needed_reason"),
|
||||||
|
textarea: document.getElementById("id_action_needed_reason_email"),
|
||||||
|
lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"),
|
||||||
|
modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"),
|
||||||
|
apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null,
|
||||||
|
textAreaFormGroup: document.querySelector('.field-action_needed_reason'),
|
||||||
|
dropdownFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
||||||
|
statusToCheck: "action needed",
|
||||||
|
sessionVariableName: "showActionNeededReason",
|
||||||
|
apiErrorMessage: "Error when attempting to grab action needed email: "
|
||||||
|
}
|
||||||
|
super(emailConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadActionNeededEmail() {
|
||||||
|
// Hide/show the email fields depending on the current status
|
||||||
|
this.initializeFormGroups();
|
||||||
|
// Setup the textarea, edit button, helper text
|
||||||
|
this.updateUserInterface();
|
||||||
|
this.initializeDropdown();
|
||||||
|
this.initializeModalConfirm();
|
||||||
|
this.initializeDirectEditButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when no reason is selected
|
||||||
|
showPlaceholderNoReason() {
|
||||||
|
this.showPlaceholder("Email:", "Select an action needed reason to see email");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when the reason other is selected
|
||||||
|
showPlaceholderOtherReason() {
|
||||||
|
this.showPlaceholder("Email:", "No email will be sent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
||||||
|
* This shows the auto generated email on action needed reason.
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const domainRequestForm = document.getElementById("domainrequest_form");
|
||||||
|
if (!domainRequestForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
const customEmail = new customActionNeededEmail();
|
||||||
|
|
||||||
|
// Check that every variable was setup correctly
|
||||||
|
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||||
|
if (nullItems.length > 0) {
|
||||||
|
console.error(`Failed to load customActionNeededEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customEmail.loadActionNeededEmail()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
class customRejectedEmail extends CustomizableEmailBase {
|
||||||
|
constructor() {
|
||||||
|
const emailConfig = {
|
||||||
|
dropdown: document.getElementById("id_rejection_reason"),
|
||||||
|
textarea: document.getElementById("id_rejection_reason_email"),
|
||||||
|
lastSentEmailContent: document.getElementById("last-sent-rejection-email-content"),
|
||||||
|
modalConfirm: document.getElementById("rejection-reason__confirm-edit-email"),
|
||||||
|
apiUrl: document.getElementById("get-rejection-email-for-user-json")?.value || null,
|
||||||
|
textAreaFormGroup: document.querySelector('.field-rejection_reason'),
|
||||||
|
dropdownFormGroup: document.querySelector('.field-rejection_reason_email'),
|
||||||
|
statusToCheck: "rejected",
|
||||||
|
sessionVariableName: "showRejectionReason",
|
||||||
|
errorMessage: "Error when attempting to grab rejected email: "
|
||||||
|
};
|
||||||
|
super(emailConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRejectedEmail() {
|
||||||
|
this.initializeFormGroups();
|
||||||
|
this.updateUserInterface();
|
||||||
|
this.initializeDropdown();
|
||||||
|
this.initializeModalConfirm();
|
||||||
|
this.initializeDirectEditButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the placeholder text when no reason is selected
|
||||||
|
showPlaceholderNoReason() {
|
||||||
|
this.showPlaceholder("Email:", "Select a rejection reason to see email");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserInterface(reason=this.dropdown.value, excluded_reasons=[]) {
|
||||||
|
super.updateUserInterface(reason, excluded_reasons);
|
||||||
|
}
|
||||||
|
// Overrides the placeholder text when the reason other is selected
|
||||||
|
// showPlaceholderOtherReason() {
|
||||||
|
// this.showPlaceholder("Email:", "No email will be sent");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** An IIFE that hooks to the show/hide button underneath rejected reason.
|
||||||
|
* This shows the auto generated email on action needed reason.
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const domainRequestForm = document.getElementById("domainrequest_form");
|
||||||
|
if (!domainRequestForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
const customEmail = new customRejectedEmail();
|
||||||
|
// Check that every variable was setup correctly
|
||||||
|
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||||
|
if (nullItems.length > 0) {
|
||||||
|
console.error(`Failed to load customRejectedEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customEmail.loadRejectedEmail()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -706,18 +854,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// Extract the submitter name, title, email, and phone number
|
|
||||||
const submitterDiv = document.querySelector('.form-row.field-submitter');
|
|
||||||
const submitterNameElement = document.getElementById('id_submitter');
|
|
||||||
// We have to account for different superuser and analyst markups
|
|
||||||
const submitterName = submitterNameElement
|
|
||||||
? submitterNameElement.options[submitterNameElement.selectedIndex].text
|
|
||||||
: submitterDiv.querySelector('a').text;
|
|
||||||
const submitterTitle = extractTextById('contact_info_title', submitterDiv);
|
|
||||||
const submitterEmail = extractTextById('contact_info_email', submitterDiv);
|
|
||||||
const submitterPhone = extractTextById('contact_info_phone', submitterDiv);
|
|
||||||
let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`;
|
|
||||||
|
|
||||||
|
|
||||||
//------ Senior Official
|
//------ Senior Official
|
||||||
const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official');
|
const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official');
|
||||||
|
@ -734,7 +870,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
`<strong>Current Websites:</strong> ${existingWebsites.join(', ')}</br>` +
|
`<strong>Current Websites:</strong> ${existingWebsites.join(', ')}</br>` +
|
||||||
`<strong>Rationale:</strong></br>` +
|
`<strong>Rationale:</strong></br>` +
|
||||||
`<strong>Alternative Domains:</strong> ${alternativeDomains.join(', ')}</br>` +
|
`<strong>Alternative Domains:</strong> ${alternativeDomains.join(', ')}</br>` +
|
||||||
`<strong>Submitter:</strong> ${submitterInfo}</br>` +
|
|
||||||
`<strong>Senior Official:</strong> ${seniorOfficialInfo}</br>` +
|
`<strong>Senior Official:</strong> ${seniorOfficialInfo}</br>` +
|
||||||
`<strong>Other Employees:</strong> ${otherContactsSummary}</br>`;
|
`<strong>Other Employees:</strong> ${otherContactsSummary}</br>`;
|
||||||
|
|
||||||
|
|
|
@ -385,6 +385,7 @@ a.button,
|
||||||
font-kerning: auto;
|
font-kerning: auto;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
.button svg,
|
.button svg,
|
||||||
.button span,
|
.button span,
|
||||||
|
@ -392,6 +393,9 @@ a.button,
|
||||||
.usa-button--dja span {
|
.usa-button--dja span {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
.usa-button--dja.usa-button--unstyled {
|
||||||
|
color: var(--link-fg);
|
||||||
|
}
|
||||||
.usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) {
|
.usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) {
|
||||||
background: var(--button-bg);
|
background: var(--button-bg);
|
||||||
}
|
}
|
||||||
|
@ -421,11 +425,34 @@ input[type=submit].button--dja-toolbar {
|
||||||
input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover {
|
input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover {
|
||||||
border-color: var(--body-quiet-color);
|
border-color: var(--body-quiet-color);
|
||||||
}
|
}
|
||||||
// Targets the DJA buttom with a nested icon
|
.admin-icon-group {
|
||||||
button .usa-icon,
|
position: relative;
|
||||||
.button .usa-icon,
|
display: inline;
|
||||||
.button--clipboard .usa-icon {
|
align-items: center;
|
||||||
vertical-align: middle;
|
|
||||||
|
input {
|
||||||
|
// Allow for padding around the copy button
|
||||||
|
padding-right: 35px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding-left: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.usa-button__small-text,
|
||||||
|
.usa-button__small-text span {
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module--custom {
|
.module--custom {
|
||||||
|
@ -673,71 +700,10 @@ address.dja-address-contact-list {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the clipboard button "float" inside of the input box
|
|
||||||
.admin-icon-group {
|
|
||||||
position: relative;
|
|
||||||
display: inline;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
input {
|
|
||||||
// Allow for padding around the copy button
|
|
||||||
padding-right: 35px !important;
|
|
||||||
// Match the height of other inputs
|
|
||||||
min-height: 2.25rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
line-height: 14px;
|
|
||||||
width: max-content;
|
|
||||||
font-size: unset;
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
padding-left: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-icon-group.admin-icon-group__clipboard-link {
|
|
||||||
position: relative;
|
|
||||||
display: inline;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
|
|
||||||
.usa-button--icon {
|
|
||||||
position: absolute;
|
|
||||||
right: auto;
|
|
||||||
left: 4px;
|
|
||||||
height: 100%;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
font-size: unset !important;
|
|
||||||
display: inline-flex;
|
|
||||||
padding-top: 4px;
|
|
||||||
line-height: 14px;
|
|
||||||
width: max-content;
|
|
||||||
font-size: unset;
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-outline-on-click:focus {
|
.no-outline-on-click:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-button__small-text {
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get rid of padding on all help texts
|
// Get rid of padding on all help texts
|
||||||
form .aligned p.help, form .aligned div.help {
|
form .aligned p.help, form .aligned div.help {
|
||||||
padding-left: 0px !important;
|
padding-left: 0px !important;
|
||||||
|
@ -887,6 +853,9 @@ div.dja__model-description{
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.padding-bottom-0 {
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-container {
|
.flex-container {
|
||||||
@media screen and (min-width: 700px) and (max-width: 1150px) {
|
@media screen and (min-width: 700px) and (max-width: 1150px) {
|
||||||
|
|
|
@ -31,9 +31,10 @@ from registrar.views.utility.api_views import (
|
||||||
get_senior_official_from_federal_agency_json,
|
get_senior_official_from_federal_agency_json,
|
||||||
get_federal_and_portfolio_types_from_federal_agency_json,
|
get_federal_and_portfolio_types_from_federal_agency_json,
|
||||||
get_action_needed_email_for_user_json,
|
get_action_needed_email_for_user_json,
|
||||||
|
get_rejection_email_for_user_json,
|
||||||
)
|
)
|
||||||
|
|
||||||
from registrar.views.domain_request import Step
|
from registrar.views.domain_request import Step, PortfolioDomainRequestStep
|
||||||
from registrar.views.transfer_user import TransferUserView
|
from registrar.views.transfer_user import TransferUserView
|
||||||
from registrar.views.utility import always_404
|
from registrar.views.utility import always_404
|
||||||
from api.views import available, rdap, get_current_federal, get_current_full
|
from api.views import available, rdap, get_current_federal, get_current_full
|
||||||
|
@ -61,6 +62,9 @@ for step, view in [
|
||||||
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
||||||
(Step.REQUIREMENTS, views.Requirements),
|
(Step.REQUIREMENTS, views.Requirements),
|
||||||
(Step.REVIEW, views.Review),
|
(Step.REVIEW, views.Review),
|
||||||
|
# Portfolio steps
|
||||||
|
(PortfolioDomainRequestStep.REQUESTING_ENTITY, views.RequestingEntity),
|
||||||
|
(PortfolioDomainRequestStep.ADDITIONAL_DETAILS, views.PortfolioAdditionalDetails),
|
||||||
]:
|
]:
|
||||||
domain_request_urls.append(path(f"{step}/", view.as_view(), name=step))
|
domain_request_urls.append(path(f"{step}/", view.as_view(), name=step))
|
||||||
|
|
||||||
|
@ -192,6 +196,11 @@ urlpatterns = [
|
||||||
get_action_needed_email_for_user_json,
|
get_action_needed_email_for_user_json,
|
||||||
name="get-action-needed-email-for-user-json",
|
name="get-action-needed-email-for-user-json",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"admin/api/get-rejection-email-for-user-json/",
|
||||||
|
get_rejection_email_for_user_json,
|
||||||
|
name="get-rejection-email-for-user-json",
|
||||||
|
),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path(
|
path(
|
||||||
"reports/export_data_type_user/",
|
"reports/export_data_type_user/",
|
||||||
|
@ -204,7 +213,12 @@ urlpatterns = [
|
||||||
name="export_data_type_requests",
|
name="export_data_type_requests",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/<id>/edit/",
|
"reports/export_data_type_requests/",
|
||||||
|
ExportDataTypeRequests.as_view(),
|
||||||
|
name="export_data_type_requests",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"domain-request/<int:id>/edit/",
|
||||||
views.DomainRequestWizard.as_view(),
|
views.DomainRequestWizard.as_view(),
|
||||||
name=views.DomainRequestWizard.EDIT_URL_NAME,
|
name=views.DomainRequestWizard.EDIT_URL_NAME,
|
||||||
),
|
),
|
||||||
|
|
|
@ -21,6 +21,13 @@ from registrar.utility.constants import BranchChoices
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestingEntityForm(RegistrarForm):
|
||||||
|
organization_name = forms.CharField(
|
||||||
|
label="Organization name",
|
||||||
|
error_messages={"required": "Enter the name of your organization."},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationTypeForm(RegistrarForm):
|
class OrganizationTypeForm(RegistrarForm):
|
||||||
generic_org_type = forms.ChoiceField(
|
generic_org_type = forms.ChoiceField(
|
||||||
# use the long names in the domain request form
|
# use the long names in the domain request form
|
||||||
|
|
|
@ -279,11 +279,11 @@ class BaseYesNoForm(RegistrarForm):
|
||||||
return initial_value
|
return initial_value
|
||||||
|
|
||||||
|
|
||||||
def request_step_list(request_wizard):
|
def request_step_list(request_wizard, step_enum):
|
||||||
"""Dynamically generated list of steps in the form wizard."""
|
"""Dynamically generated list of steps in the form wizard."""
|
||||||
step_list = []
|
step_list = []
|
||||||
for step in request_wizard.StepEnum:
|
for step in step_enum:
|
||||||
condition = request_wizard.WIZARD_CONDITIONS.get(step, True)
|
condition = request_wizard.wizard_conditions.get(step, True)
|
||||||
if callable(condition):
|
if callable(condition):
|
||||||
condition = condition(request_wizard)
|
condition = condition(request_wizard)
|
||||||
if condition:
|
if condition:
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-10-08 18:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0132_alter_domaininformation_portfolio_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="rejection_reason_email",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="rejection_reason",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("domain_purpose", "Purpose requirements not met"),
|
||||||
|
("requestor_not_eligible", "Requestor not eligible to make request"),
|
||||||
|
("org_has_domain", "Org already has a .gov domain"),
|
||||||
|
("contacts_not_verified", "Org contacts couldn't be verified"),
|
||||||
|
("org_not_eligible", "Org not eligible for a .gov domain"),
|
||||||
|
("naming_requirements", "Naming requirements not met"),
|
||||||
|
("other", "Other/Unspecified"),
|
||||||
|
],
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 4.2.10 on 2024-10-08 19:05
|
# Generated by Django 4.2.10 on 2024-10-11 19:58
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("registrar", "0132_alter_domaininformation_portfolio_and_more"),
|
("registrar", "0133_domainrequest_rejection_reason_email_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -254,18 +254,18 @@ class DomainRequest(TimeStampedModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
class RejectionReasons(models.TextChoices):
|
class RejectionReasons(models.TextChoices):
|
||||||
DOMAIN_PURPOSE = "purpose_not_met", "Purpose requirements not met"
|
DOMAIN_PURPOSE = "domain_purpose", "Purpose requirements not met"
|
||||||
REQUESTOR = "requestor_not_eligible", "Requestor not eligible to make request"
|
REQUESTOR_NOT_ELIGIBLE = "requestor_not_eligible", "Requestor not eligible to make request"
|
||||||
SECOND_DOMAIN_REASONING = (
|
ORG_HAS_DOMAIN = (
|
||||||
"org_has_domain",
|
"org_has_domain",
|
||||||
"Org already has a .gov domain",
|
"Org already has a .gov domain",
|
||||||
)
|
)
|
||||||
CONTACTS_OR_ORGANIZATION_LEGITIMACY = (
|
CONTACTS_NOT_VERIFIED = (
|
||||||
"contacts_not_verified",
|
"contacts_not_verified",
|
||||||
"Org contacts couldn't be verified",
|
"Org contacts couldn't be verified",
|
||||||
)
|
)
|
||||||
ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain"
|
ORG_NOT_ELIGIBLE = "org_not_eligible", "Org not eligible for a .gov domain"
|
||||||
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
|
NAMING_REQUIREMENTS = "naming_requirements", "Naming requirements not met"
|
||||||
OTHER = "other", "Other/Unspecified"
|
OTHER = "other", "Other/Unspecified"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -300,6 +300,11 @@ class DomainRequest(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rejection_reason_email = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
action_needed_reason = models.TextField(
|
action_needed_reason = models.TextField(
|
||||||
choices=ActionNeededReasons.choices,
|
choices=ActionNeededReasons.choices,
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -635,15 +640,16 @@ class DomainRequest(TimeStampedModel):
|
||||||
# Actually updates the organization_type field
|
# Actually updates the organization_type field
|
||||||
org_type_helper.create_or_update_organization_type()
|
org_type_helper.create_or_update_organization_type()
|
||||||
|
|
||||||
def _cache_status_and_action_needed_reason(self):
|
def _cache_status_and_status_reasons(self):
|
||||||
"""Maintains a cache of properties so we can avoid a DB call"""
|
"""Maintains a cache of properties so we can avoid a DB call"""
|
||||||
self._cached_action_needed_reason = self.action_needed_reason
|
self._cached_action_needed_reason = self.action_needed_reason
|
||||||
|
self._cached_rejection_reason = self.rejection_reason
|
||||||
self._cached_status = self.status
|
self._cached_status = self.status
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Store original values for caching purposes. Used to compare them on save.
|
# Store original values for caching purposes. Used to compare them on save.
|
||||||
self._cache_status_and_action_needed_reason()
|
self._cache_status_and_status_reasons()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Save override for custom properties"""
|
"""Save override for custom properties"""
|
||||||
|
@ -655,23 +661,63 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Handle the action needed email.
|
# Handle custom status emails.
|
||||||
# An email is sent out when action_needed_reason is changed or added.
|
# An email is sent out when a, for example, action_needed_reason is changed or added.
|
||||||
if self.action_needed_reason and self.status == self.DomainRequestStatus.ACTION_NEEDED:
|
statuses_that_send_custom_emails = [self.DomainRequestStatus.ACTION_NEEDED, self.DomainRequestStatus.REJECTED]
|
||||||
self.sync_action_needed_reason()
|
if self.status in statuses_that_send_custom_emails:
|
||||||
|
self.send_custom_status_update_email(self.status)
|
||||||
|
|
||||||
# Update the cached values after saving
|
# Update the cached values after saving
|
||||||
self._cache_status_and_action_needed_reason()
|
self._cache_status_and_status_reasons()
|
||||||
|
|
||||||
def sync_action_needed_reason(self):
|
def send_custom_status_update_email(self, status):
|
||||||
"""Checks if we need to send another action needed email"""
|
"""Helper function to send out a second status email when the status remains the same,
|
||||||
was_already_action_needed = self._cached_status == self.DomainRequestStatus.ACTION_NEEDED
|
but the reason has changed."""
|
||||||
reason_exists = self._cached_action_needed_reason is not None and self.action_needed_reason is not None
|
|
||||||
reason_changed = self._cached_action_needed_reason != self.action_needed_reason
|
# Currently, we store all this information in three variables.
|
||||||
if was_already_action_needed and reason_exists and reason_changed:
|
# When adding new reasons, this can be a lot to manage so we store it here
|
||||||
# We don't send emails out in state "other"
|
# in a centralized location. However, this may need to change if this scales.
|
||||||
if self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
status_information = {
|
||||||
self._send_action_needed_reason_email(email_content=self.action_needed_reason_email)
|
self.DomainRequestStatus.ACTION_NEEDED: {
|
||||||
|
"cached_reason": self._cached_action_needed_reason,
|
||||||
|
"reason": self.action_needed_reason,
|
||||||
|
"email": self.action_needed_reason_email,
|
||||||
|
"excluded_reasons": [DomainRequest.ActionNeededReasons.OTHER],
|
||||||
|
"wrap_email": True,
|
||||||
|
},
|
||||||
|
self.DomainRequestStatus.REJECTED: {
|
||||||
|
"cached_reason": self._cached_rejection_reason,
|
||||||
|
"reason": self.rejection_reason,
|
||||||
|
"email": self.rejection_reason_email,
|
||||||
|
"excluded_reasons": [],
|
||||||
|
# "excluded_reasons": [DomainRequest.RejectionReasons.OTHER],
|
||||||
|
"wrap_email": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
status_info = status_information.get(status)
|
||||||
|
|
||||||
|
# Don't send an email if there is nothing to send.
|
||||||
|
if status_info.get("email") is None:
|
||||||
|
logger.warning("send_custom_status_update_email() => Tried sending an empty email.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# We should never send an email if no reason was specified.
|
||||||
|
# Additionally, Don't send out emails for reasons that shouldn't send them.
|
||||||
|
if status_info.get("reason") is None or status_info.get("reason") in status_info.get("excluded_reasons"):
|
||||||
|
logger.warning("send_custom_status_update_email() => Tried sending a status email without a reason.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only send out an email if the underlying reason itself changed or if no email was sent previously.
|
||||||
|
if status_info.get("cached_reason") != status_info.get("reason") or status_info.get("cached_reason") is None:
|
||||||
|
bcc_address = settings.DEFAULT_FROM_EMAIL if settings.IS_PRODUCTION else ""
|
||||||
|
self._send_status_update_email(
|
||||||
|
new_status=status,
|
||||||
|
email_template="emails/includes/custom_email.txt",
|
||||||
|
email_template_subject="emails/status_change_subject.txt",
|
||||||
|
bcc_address=bcc_address,
|
||||||
|
custom_email_content=status_info.get("email"),
|
||||||
|
wrap_email=status_information.get("wrap_email"),
|
||||||
|
)
|
||||||
|
|
||||||
def sync_yes_no_form_fields(self):
|
def sync_yes_no_form_fields(self):
|
||||||
"""Some yes/no forms use a db field to track whether it was checked or not.
|
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||||
|
@ -901,7 +947,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
target=DomainRequestStatus.ACTION_NEEDED,
|
target=DomainRequestStatus.ACTION_NEEDED,
|
||||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||||
)
|
)
|
||||||
def action_needed(self, send_email=True):
|
def action_needed(self):
|
||||||
"""Send back an domain request that is under investigation or rejected.
|
"""Send back an domain request that is under investigation or rejected.
|
||||||
|
|
||||||
This action is logged.
|
This action is logged.
|
||||||
|
@ -909,43 +955,23 @@ class DomainRequest(TimeStampedModel):
|
||||||
This action cleans up the rejection status if moving away from rejected.
|
This action cleans up the rejection status if moving away from rejected.
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade) when they exist."""
|
(will cascade) when they exist.
|
||||||
|
|
||||||
|
Afterwards, we send out an email for action_needed in def save().
|
||||||
|
See the function send_custom_status_update_email.
|
||||||
|
"""
|
||||||
|
|
||||||
if self.status == self.DomainRequestStatus.APPROVED:
|
if self.status == self.DomainRequestStatus.APPROVED:
|
||||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
self.delete_and_clean_up_domain("action_needed")
|
||||||
elif self.status == self.DomainRequestStatus.REJECTED:
|
elif self.status == self.DomainRequestStatus.REJECTED:
|
||||||
self.rejection_reason = None
|
self.rejection_reason = None
|
||||||
|
|
||||||
|
# Check if the tuple is setup correctly, then grab its value.
|
||||||
|
|
||||||
literal = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
literal = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
||||||
# Check if the tuple is setup correctly, then grab its value
|
|
||||||
action_needed = literal if literal is not None else "Action Needed"
|
action_needed = literal if literal is not None else "Action Needed"
|
||||||
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
|
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
|
||||||
|
|
||||||
# Send out an email if an action needed reason exists
|
|
||||||
if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
|
||||||
email_content = self.action_needed_reason_email
|
|
||||||
self._send_action_needed_reason_email(send_email, email_content)
|
|
||||||
|
|
||||||
def _send_action_needed_reason_email(self, send_email=True, email_content=None):
|
|
||||||
"""Sends out an automatic email for each valid action needed reason provided"""
|
|
||||||
|
|
||||||
email_template_name = "custom_email.txt"
|
|
||||||
email_template_subject_name = f"{self.action_needed_reason}_subject.txt"
|
|
||||||
|
|
||||||
bcc_address = ""
|
|
||||||
if settings.IS_PRODUCTION:
|
|
||||||
bcc_address = settings.DEFAULT_FROM_EMAIL
|
|
||||||
|
|
||||||
self._send_status_update_email(
|
|
||||||
new_status="action needed",
|
|
||||||
email_template=f"emails/action_needed_reasons/{email_template_name}",
|
|
||||||
email_template_subject=f"emails/action_needed_reasons/{email_template_subject_name}",
|
|
||||||
send_email=send_email,
|
|
||||||
bcc_address=bcc_address,
|
|
||||||
custom_email_content=email_content,
|
|
||||||
wrap_email=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[
|
source=[
|
||||||
|
@ -1039,18 +1065,20 @@ class DomainRequest(TimeStampedModel):
|
||||||
def reject(self):
|
def reject(self):
|
||||||
"""Reject an domain request that has been submitted.
|
"""Reject an domain request that has been submitted.
|
||||||
|
|
||||||
|
This action is logged.
|
||||||
|
|
||||||
|
This action cleans up the action needed status if moving away from action needed.
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade), and send an email notification."""
|
(will cascade) when they exist.
|
||||||
|
|
||||||
|
Afterwards, we send out an email for reject in def save().
|
||||||
|
See the function send_custom_status_update_email.
|
||||||
|
"""
|
||||||
|
|
||||||
if self.status == self.DomainRequestStatus.APPROVED:
|
if self.status == self.DomainRequestStatus.APPROVED:
|
||||||
self.delete_and_clean_up_domain("reject")
|
self.delete_and_clean_up_domain("reject")
|
||||||
|
|
||||||
self._send_status_update_email(
|
|
||||||
"action needed",
|
|
||||||
"emails/status_change_rejected.txt",
|
|
||||||
"emails/status_change_rejected_subject.txt",
|
|
||||||
)
|
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status",
|
field="status",
|
||||||
source=[
|
source=[
|
||||||
|
|
|
@ -20,10 +20,11 @@
|
||||||
</li>
|
</li>
|
||||||
{% if opts.model_name == 'domainrequest' %}
|
{% if opts.model_name == 'domainrequest' %}
|
||||||
<li>
|
<li>
|
||||||
<a id="id-copy-to-clipboard-summary" class="button--clipboard" type="button" href="#">
|
<a id="id-copy-to-clipboard-summary" class="usa-button--dja" type="button" href="#">
|
||||||
<svg class="usa-icon" >
|
<svg class="usa-icon" >
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
<span>{% translate "Copy request summary" %}</span>
|
<span>{% translate "Copy request summary" %}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -8,7 +8,7 @@ Template for an input field with a clipboard
|
||||||
<div class="admin-icon-group">
|
<div class="admin-icon-group">
|
||||||
{{ field }}
|
{{ field }}
|
||||||
<button
|
<button
|
||||||
class="usa-button usa-button--unstyled padding-left-1 usa-button--icon button--clipboard copy-to-clipboard"
|
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled padding-left-1 usa-button--icon copy-to-clipboard"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div class="no-outline-on-click">
|
<div class="no-outline-on-click">
|
||||||
|
@ -17,23 +17,27 @@ Template for an input field with a clipboard
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Copy
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
|
<span>Copy</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="admin-icon-group admin-icon-group__clipboard-link">
|
<div class="admin-icon-group">
|
||||||
<input aria-hidden="true" class="display-none" value="{{ field.email }}" />
|
<input aria-hidden="true" class="display-none" value="{{ field.email }}" />
|
||||||
<button
|
{% if field.email is not None %}
|
||||||
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard text-no-underline padding-left-05"
|
<button
|
||||||
type="button"
|
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled padding-right-1 usa-button--icon copy-to-clipboard text-no-underline padding-left-05"
|
||||||
>
|
type="button"
|
||||||
<svg
|
|
||||||
class="usa-icon"
|
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<svg
|
||||||
</svg>
|
class="usa-icon"
|
||||||
Copy
|
>
|
||||||
</button>
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
|
</svg>
|
||||||
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
|
<span>Copy</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
<input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/>
|
<input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/>
|
||||||
{% url 'get-action-needed-email-for-user-json' as url %}
|
{% url 'get-action-needed-email-for-user-json' as url %}
|
||||||
<input id="get-action-needed-email-for-user-json" class="display-none" value="{{ url }}" />
|
<input id="get-action-needed-email-for-user-json" class="display-none" value="{{ url }}" />
|
||||||
|
{% url 'get-rejection-email-for-user-json' as url_2 %}
|
||||||
|
<input id="get-rejection-email-for-user-json" class="display-none" value="{{ url_2 }}" />
|
||||||
{% for fieldset in adminform %}
|
{% for fieldset in adminform %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
TODO: this will eventually need to be changed to something like this
|
TODO: this will eventually need to be changed to something like this
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
{% if user.email %}
|
{% if user.email %}
|
||||||
<span id="contact_info_email">{{ user.email }}</span>
|
<span id="contact_info_email">{{ user.email }}</span>
|
||||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||||
<br class="admin-icon-group__br">
|
<br>
|
||||||
{% else %}
|
{% else %}
|
||||||
None<br>
|
None<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -137,29 +137,28 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
|
|
||||||
{% block field_other %}
|
{% block field_other %}
|
||||||
{% if field.field.name == "action_needed_reason_email" %}
|
{% if field.field.name == "action_needed_reason_email" %}
|
||||||
|
{{ field.field }}
|
||||||
|
|
||||||
<div class="margin-top-05 text-faded field-action_needed_reason_email__placeholder">
|
<div class="margin-top-05 text-faded custom-email-placeholder">
|
||||||
–
|
–
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ field.field }}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="Edit email in textarea"
|
aria-label="Edit email in textarea"
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline field-action_needed_reason_email__edit flex-align-self-start"
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline flex-align-self-start edit-email-button"
|
||||||
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#email-already-sent-modal"
|
href="#action-needed-email-already-sent-modal"
|
||||||
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 field-action_needed_reason_email__modal-trigger flex-align-self-start"
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 edit-button-modal-trigger flex-align-self-start"
|
||||||
aria-controls="email-already-sent-modal"
|
aria-controls="action-needed-email-already-sent-modal"
|
||||||
data-open-modal
|
data-open-modal
|
||||||
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="usa-modal"
|
class="usa-modal"
|
||||||
id="email-already-sent-modal"
|
id="action-needed-email-already-sent-modal"
|
||||||
aria-labelledby="Are you sure you want to edit this email?"
|
aria-labelledby="Are you sure you want to edit this email?"
|
||||||
aria-describedby="The creator of this request already received an email"
|
aria-describedby="The creator of this request already received an email"
|
||||||
>
|
>
|
||||||
|
@ -187,8 +186,8 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
id="action-needed-reason__confirm-edit-email"
|
||||||
class="usa-button"
|
class="usa-button"
|
||||||
id="confirm-edit-email"
|
|
||||||
data-close-modal
|
data-close-modal
|
||||||
>
|
>
|
||||||
Yes, continue editing
|
Yes, continue editing
|
||||||
|
@ -221,11 +220,99 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if original_object.action_needed_reason_email %}
|
{% if original_object.action_needed_reason_email %}
|
||||||
<input id="last-sent-email-content" class="display-none" value="{{original_object.action_needed_reason_email}}">
|
<input id="last-sent-action-needed-email-content" class="display-none" value="{{original_object.action_needed_reason_email}}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<input id="last-sent-email-content" class="display-none" value="None">
|
<input id="last-sent-action-needed-email-content" class="display-none" value="None">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% elif field.field.name == "rejection_reason_email" %}
|
||||||
|
{{ field.field }}
|
||||||
|
|
||||||
|
<div class="margin-top-05 text-faded custom-email-placeholder">
|
||||||
|
–
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
aria-label="Edit email in textarea"
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline flex-align-self-start edit-email-button"
|
||||||
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#rejection-reason-email-already-sent-modal"
|
||||||
|
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 edit-button-modal-trigger flex-align-self-start"
|
||||||
|
aria-controls="rejection-reason-email-already-sent-modal"
|
||||||
|
data-open-modal
|
||||||
|
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="rejection-reason-email-already-sent-modal"
|
||||||
|
aria-labelledby="Are you sure you want to edit this email?"
|
||||||
|
aria-describedby="The creator of this request already received an email"
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading">
|
||||||
|
Are you sure you want to edit this email?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p>
|
||||||
|
The creator of this request already received an email for this status/reason:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="font-body-sm">Status: <b>Rejected</b></li>
|
||||||
|
<li class="font-body-sm">Reason: <b>{{ original_object.get_rejection_reason_display }}</b></li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you edit this email's text, <b>the system will send another email</b> to
|
||||||
|
the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="rejection-reason__confirm-edit-email"
|
||||||
|
class="usa-button"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Yes, continue editing
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
name="_cancel_edit_email"
|
||||||
|
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="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if original_object.rejection_reason_email %}
|
||||||
|
<input id="last-sent-rejection-email-content" class="display-none" value="{{original_object.rejection_reason_email}}">
|
||||||
|
{% else %}
|
||||||
|
<input id="last-sent-rejection-email-content" class="display-none" value="None">
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field.field }}
|
{{ field.field }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -254,7 +341,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4">Other contact information</th>
|
<th colspan="5">Other contact information</th>
|
||||||
<tr>
|
<tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -267,18 +354,31 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
</td>
|
</td>
|
||||||
<td class="padding-left-1">{{ contact.phone }}</td>
|
<td class="padding-left-1">{{ contact.phone }}</td>
|
||||||
<td class="padding-left-1 text-size-small">
|
<td class="padding-left-1 text-size-small">
|
||||||
<input aria-hidden="true" class="display-none" value="{{ contact.email }}" />
|
{% if contact.email %}
|
||||||
<button
|
<input aria-hidden="true" class="display-none" value="{{ contact.email }}" />
|
||||||
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard usa-button__small-text text-no-underline"
|
<button
|
||||||
type="button"
|
class="
|
||||||
>
|
usa-button--dja
|
||||||
<svg
|
usa-button
|
||||||
class="usa-icon"
|
usa-button__small-text
|
||||||
|
usa-button--unstyled
|
||||||
|
padding-right-1
|
||||||
|
padding-top-0
|
||||||
|
padding-bottom-0
|
||||||
|
usa-button--icon
|
||||||
|
copy-to-clipboard
|
||||||
|
text-no-underline"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<svg
|
||||||
</svg>
|
class="usa-icon"
|
||||||
<span>Copy email</span>
|
>
|
||||||
</button>
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
|
</svg>
|
||||||
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
|
<span>Copy email</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -12,8 +12,10 @@
|
||||||
|
|
||||||
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
|
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
|
||||||
{% if not is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}</p>
|
{% if not is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}</p>
|
||||||
|
|
||||||
|
{% if not portfolio %}
|
||||||
<p>Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
<p>Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p>Note that <strong>only federal agencies can request generic terms</strong> like
|
<p>Note that <strong>only federal agencies can request generic terms</strong> like
|
||||||
vote.gov.</p>
|
vote.gov.</p>
|
||||||
|
|
|
@ -12,7 +12,11 @@
|
||||||
|
|
||||||
<h1>You’re about to start your .gov domain request.</h1>
|
<h1>You’re about to start your .gov domain request.</h1>
|
||||||
<p>You don’t have to complete the process in one session. You can save what you enter and come back to it when you’re ready.</p>
|
<p>You don’t have to complete the process in one session. You can save what you enter and come back to it when you’re ready.</p>
|
||||||
|
{% if portfolio %}
|
||||||
|
<p>We’ll use the information you provide to verify your domain request meets our guidelines.</p>
|
||||||
|
{% else %}
|
||||||
<p>We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.</p>
|
<p>We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.</p>
|
||||||
|
{% endif %}
|
||||||
<h2>Time to complete the form</h2>
|
<h2>Time to complete the form</h2>
|
||||||
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
|
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
|
||||||
completing your domain request might take around 15 minutes.</p>
|
completing your domain request might take around 15 minutes.</p>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'domain_request_form.html' %}
|
||||||
|
{% load field_helpers url_helpers %}
|
||||||
|
|
||||||
|
{% block form_instructions %}
|
||||||
|
<p>🛸🛸🛸🛸 Placeholder content 🛸🛸🛸🛸</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form_fields %}
|
||||||
|
<fieldset class="usa-fieldset">
|
||||||
|
<legend>
|
||||||
|
<h2>What is the name of your space vessel?</h2>
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
{% input_with_errors forms.0.organization_name %}
|
||||||
|
</fieldset>
|
||||||
|
{% endblock %}
|
|
@ -19,5 +19,9 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
{% include "includes/request_review_steps.html" with is_editable=True %}
|
{% if portfolio %}
|
||||||
|
{% include "includes/portfolio_request_review_steps.html" with is_editable=True %}
|
||||||
|
{% else %}
|
||||||
|
{% include "includes/request_review_steps.html" with is_editable=True %}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -8,8 +8,8 @@ REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Rejected
|
STATUS: Rejected
|
||||||
|
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
{% if domain_request.rejection_reason != 'other' %}
|
{% if reason != domain_request.RejectionReasons.DOMAIN_PURPOSE.OTHER %}
|
||||||
REJECTION REASON{% endif %}{% if domain_request.rejection_reason == 'purpose_not_met' %}
|
REJECTION REASON{% endif %}{% if reason == domain_request.RejectionReasons.DOMAIN_PURPOSE %}
|
||||||
Your domain request was rejected because the purpose you provided did not meet our
|
Your domain request was rejected because the purpose you provided did not meet our
|
||||||
requirements. You didn’t provide enough information about how you intend to use the
|
requirements. You didn’t provide enough information about how you intend to use the
|
||||||
domain.
|
domain.
|
||||||
|
@ -18,7 +18,7 @@ Learn more about:
|
||||||
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
||||||
- What you can and can’t do with .gov domains <https://get.gov/domains/requirements/>
|
- What you can and can’t do with .gov domains <https://get.gov/domains/requirements/>
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'requestor_not_eligible' %}
|
If you have questions or comments, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.REQUESTOR_NOT_ELIGIBLE %}
|
||||||
Your domain request was rejected because we don’t believe you’re eligible to request a
|
Your domain request was rejected because we don’t believe you’re eligible to request a
|
||||||
.gov domain on behalf of {{ domain_request.organization_name }}. You must be a government employee, or be
|
.gov domain on behalf of {{ domain_request.organization_name }}. You must be a government employee, or be
|
||||||
working on behalf of a government organization, to request a .gov domain.
|
working on behalf of a government organization, to request a .gov domain.
|
||||||
|
@ -26,7 +26,7 @@ working on behalf of a government organization, to request a .gov domain.
|
||||||
|
|
||||||
DEMONSTRATE ELIGIBILITY
|
DEMONSTRATE ELIGIBILITY
|
||||||
If you can provide more information that demonstrates your eligibility, or you want to
|
If you can provide more information that demonstrates your eligibility, or you want to
|
||||||
discuss further, reply to this email.{% elif domain_request.rejection_reason == 'org_has_domain' %}
|
discuss further, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.ORG_HAS_DOMAIN %}
|
||||||
Your domain request was rejected because {{ domain_request.organization_name }} has a .gov domain. Our
|
Your domain request was rejected because {{ domain_request.organization_name }} has a .gov domain. Our
|
||||||
practice is to approve one domain per online service per government organization. We
|
practice is to approve one domain per online service per government organization. We
|
||||||
evaluate additional requests on a case-by-case basis. You did not provide sufficient
|
evaluate additional requests on a case-by-case basis. You did not provide sufficient
|
||||||
|
@ -35,9 +35,9 @@ justification for an additional domain.
|
||||||
Read more about our practice of approving one domain per online service
|
Read more about our practice of approving one domain per online service
|
||||||
<https://get.gov/domains/before/#one-domain-per-service>.
|
<https://get.gov/domains/before/#one-domain-per-service>.
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'contacts_not_verified' %}
|
If you have questions or comments, reply to this email.{% elif reason == 'contacts_not_verified' %}
|
||||||
Your domain request was rejected because we could not verify the organizational
|
Your domain request was rejected because we could not verify the organizational
|
||||||
contacts you provided. If you have questions or comments, reply to this email.{% elif domain_request.rejection_reason == 'org_not_eligible' %}
|
contacts you provided. If you have questions or comments, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.ORG_NOT_ELIGIBLE %}
|
||||||
Your domain request was rejected because we determined that {{ domain_request.organization_name }} is not
|
Your domain request was rejected because we determined that {{ domain_request.organization_name }} is not
|
||||||
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
|
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
|
||||||
government organizations.
|
government organizations.
|
||||||
|
@ -46,7 +46,7 @@ Learn more about eligibility for .gov domains
|
||||||
<https://get.gov/domains/eligibility/>.
|
<https://get.gov/domains/eligibility/>.
|
||||||
|
|
||||||
If you have questions or comments, reply to this email.
|
If you have questions or comments, reply to this email.
|
||||||
{% elif domain_request.rejection_reason == 'naming_not_met' %}
|
{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.NAMING_REQUIREMENTS %}
|
||||||
Your domain request was rejected because it does not meet our naming requirements.
|
Your domain request was rejected because it does not meet our naming requirements.
|
||||||
Domains should uniquely identify a government organization and be clear to the
|
Domains should uniquely identify a government organization and be clear to the
|
||||||
general public. Learn more about naming requirements for your type of organization
|
general public. Learn more about naming requirements for your type of organization
|
||||||
|
@ -55,7 +55,7 @@ general public. Learn more about naming requirements for your type of organizati
|
||||||
|
|
||||||
YOU CAN SUBMIT A NEW REQUEST
|
YOU CAN SUBMIT A NEW REQUEST
|
||||||
We encourage you to request a domain that meets our requirements. If you have
|
We encourage you to request a domain that meets our requirements. If you have
|
||||||
questions or want to discuss potential domain names, reply to this email.{% elif domain_request.rejection_reason == 'other' %}
|
questions or want to discuss potential domain names, reply to this email.{% elif reason == domain_request.RejectionReasons.DOMAIN_PURPOSE.OTHER %}
|
||||||
YOU CAN SUBMIT A NEW REQUEST
|
YOU CAN SUBMIT A NEW REQUEST
|
||||||
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
|
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul class="usa-nav__primary usa-accordion">
|
<ul class="usa-nav__primary usa-accordion">
|
||||||
|
{% if not hide_domains %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% if has_any_domains_portfolio_permission %}
|
{% if has_any_domains_portfolio_permission %}
|
||||||
{% url 'domains' as url %}
|
{% url 'domains' as url %}
|
||||||
|
@ -44,13 +45,14 @@
|
||||||
Domains
|
Domains
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
<!-- <li class="usa-nav__primary-item">
|
<!-- <li class="usa-nav__primary-item">
|
||||||
<a href="#" class="usa-nav-link">
|
<a href="#" class="usa-nav-link">
|
||||||
Domain groups
|
Domain groups
|
||||||
</a>
|
</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
|
|
||||||
{% if has_organization_requests_flag %}
|
{% if has_organization_requests_flag and not hide_requests %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
<!-- user has one of the view permissions plus the edit permission, show the dropdown -->
|
<!-- user has one of the view permissions plus the edit permission, show the dropdown -->
|
||||||
{% if has_edit_request_portfolio_permission %}
|
{% if has_edit_request_portfolio_permission %}
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if step == Step.REQUESTING_ENTITY %}
|
{% if step == Step.REQUESTING_ENTITY %}
|
||||||
|
|
||||||
{% if domain_request.organization_name %}
|
{% if domain_request.organization_name %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request %}
|
{% with title=form_titles|get_item:step value=domain_request %}
|
||||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url address='true' %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url address='true' %}
|
||||||
|
@ -54,32 +53,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||||
{% with title=form_titles|get_item:step %}
|
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||||
{% if domain_request.has_additional_details %}
|
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||||
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
|
||||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
|
||||||
{% if domain_request.cisa_representative_first_name %}
|
|
||||||
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
|
||||||
{% if domain_request.cisa_representative_email %}
|
|
||||||
<li>{{domain_request.cisa_representative_email}}</li>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
No
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3 class="register-form-review-header">Anything else</h3>
|
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
|
||||||
{% if domain_request.anything_else %}
|
|
||||||
{{domain_request.anything_else}}
|
|
||||||
{% else %}
|
|
||||||
No
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
{% include "includes/summary_item.html" with title="Additional Details" value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends 'domain_request_form.html' %}
|
||||||
|
{% load static field_helpers %}
|
||||||
|
|
||||||
|
{% block form_required_fields_help_text %}
|
||||||
|
{% include "includes/required_fields.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form_fields %}
|
||||||
|
|
||||||
|
<fieldset class="usa-fieldset margin-top-2">
|
||||||
|
<legend>
|
||||||
|
<h2>Is there anything else you’d like us to know about your domain request?</h2>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="margin-top-3" id="anything-else">
|
||||||
|
<p>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
|
||||||
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors forms.0.anything_else %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -654,7 +654,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard", count=3)
|
self.assertContains(response, "copy-to-clipboard", count=3)
|
||||||
|
|
||||||
# cleanup this test
|
# cleanup this test
|
||||||
domain_info.delete()
|
domain_info.delete()
|
||||||
|
|
|
@ -535,7 +535,7 @@ class TestDomainAdminWithClient(TestCase):
|
||||||
self.assertContains(response, "Testy Tester")
|
self.assertContains(response, "Testy Tester")
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard")
|
self.assertContains(response, "copy-to-clipboard")
|
||||||
|
|
||||||
# cleanup from this test
|
# cleanup from this test
|
||||||
domain.delete()
|
domain.delete()
|
||||||
|
|
|
@ -595,7 +595,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def transition_state_and_send_email(
|
def transition_state_and_send_email(
|
||||||
self, domain_request, status, rejection_reason=None, action_needed_reason=None, action_needed_reason_email=None
|
self,
|
||||||
|
domain_request,
|
||||||
|
status,
|
||||||
|
rejection_reason=None,
|
||||||
|
action_needed_reason=None,
|
||||||
|
action_needed_reason_email=None,
|
||||||
):
|
):
|
||||||
"""Helper method for the email test cases."""
|
"""Helper method for the email test cases."""
|
||||||
|
|
||||||
|
@ -687,6 +692,10 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# We use javascript to reset the content of this. It is only automatically set
|
||||||
|
# if the email itself is somehow None.
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Test the email sent out for bad_name
|
# Test the email sent out for bad_name
|
||||||
bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
|
bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
|
||||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
|
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
|
||||||
|
@ -694,6 +703,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL
|
"DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Test the email sent out for eligibility_unclear
|
# Test the email sent out for eligibility_unclear
|
||||||
eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
|
eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
|
||||||
|
@ -702,6 +712,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL
|
"ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Test that a custom email is sent out for questionable_so
|
# Test that a custom email is sent out for questionable_so
|
||||||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||||
|
@ -710,6 +721,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, _creator.email, bcc_email_address=BCC_EMAIL
|
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, _creator.email, bcc_email_address=BCC_EMAIL
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Assert that no other emails are sent on OTHER
|
# Assert that no other emails are sent on OTHER
|
||||||
other = DomainRequest.ActionNeededReasons.OTHER
|
other = DomainRequest.ActionNeededReasons.OTHER
|
||||||
|
@ -717,6 +729,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
# Should be unchanged from before
|
# Should be unchanged from before
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Tests if an analyst can override existing email content
|
# Tests if an analyst can override existing email content
|
||||||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||||
|
@ -730,6 +743,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
domain_request.refresh_from_db()
|
domain_request.refresh_from_db()
|
||||||
self.assert_email_is_accurate("custom email content", 4, _creator.email, bcc_email_address=BCC_EMAIL)
|
self.assert_email_is_accurate("custom email content", 4, _creator.email, bcc_email_address=BCC_EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Tests if a new email gets sent when just the email is changed.
|
# Tests if a new email gets sent when just the email is changed.
|
||||||
# An email should NOT be sent out if we just modify the email content.
|
# An email should NOT be sent out if we just modify the email content.
|
||||||
|
@ -741,6 +755,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||||
|
self._reset_action_needed_email(domain_request)
|
||||||
|
|
||||||
# Set the request back to in review
|
# Set the request back to in review
|
||||||
domain_request.in_review()
|
domain_request.in_review()
|
||||||
|
@ -757,55 +772,53 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
||||||
|
|
||||||
# def test_action_needed_sends_reason_email_prod_bcc(self):
|
def _reset_action_needed_email(self, domain_request):
|
||||||
# """When an action needed reason is set, an email is sent out and help@get.gov
|
"""Sets the given action needed email back to none"""
|
||||||
# is BCC'd in production"""
|
domain_request.action_needed_reason_email = None
|
||||||
# # Ensure there is no user with this email
|
domain_request.save()
|
||||||
# EMAIL = "mayor@igorville.gov"
|
domain_request.refresh_from_db()
|
||||||
# BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
|
||||||
# User.objects.filter(email=EMAIL).delete()
|
|
||||||
# in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
|
||||||
# action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
|
||||||
|
|
||||||
# # Create a sample domain request
|
@override_settings(IS_PRODUCTION=True)
|
||||||
# domain_request = completed_domain_request(status=in_review)
|
@less_console_noise_decorator
|
||||||
|
def test_rejected_sends_reason_email_prod_bcc(self):
|
||||||
|
"""When a rejection reason is set, an email is sent out and help@get.gov
|
||||||
|
is BCC'd in production"""
|
||||||
|
# Create fake creator
|
||||||
|
EMAIL = "meoward.jones@igorville.gov"
|
||||||
|
|
||||||
# # Test the email sent out for already_has_domains
|
_creator = User.objects.create(
|
||||||
# already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
username="MrMeoward",
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains)
|
first_name="Meoward",
|
||||||
# self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
last_name="Jones",
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
email=EMAIL,
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
|
)
|
||||||
|
|
||||||
# # Test the email sent out for bad_name
|
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||||
# bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
|
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
|
rejected = DomainRequest.DomainRequestStatus.REJECTED
|
||||||
# self.assert_email_is_accurate(
|
|
||||||
# "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL
|
|
||||||
# )
|
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
|
||||||
|
|
||||||
# # Test the email sent out for eligibility_unclear
|
# Create a sample domain request
|
||||||
# eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
|
domain_request = completed_domain_request(status=in_review, user=_creator)
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=eligibility_unclear)
|
|
||||||
# self.assert_email_is_accurate(
|
|
||||||
# "ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL
|
|
||||||
# )
|
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
|
||||||
|
|
||||||
# # Test the email sent out for questionable_so
|
expected_emails = {
|
||||||
# questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
DomainRequest.RejectionReasons.DOMAIN_PURPOSE: "You didn’t provide enough information about how",
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
|
DomainRequest.RejectionReasons.REQUESTOR_NOT_ELIGIBLE: "You must be a government employee, or be",
|
||||||
# self.assert_email_is_accurate(
|
DomainRequest.RejectionReasons.ORG_HAS_DOMAIN: "practice is to approve one domain",
|
||||||
# "SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL
|
DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED: "we could not verify the organizational",
|
||||||
# )
|
DomainRequest.RejectionReasons.ORG_NOT_ELIGIBLE: ".Gov domains are only available to official U.S.-based",
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
DomainRequest.RejectionReasons.NAMING_REQUIREMENTS: "does not meet our naming requirements",
|
||||||
|
DomainRequest.RejectionReasons.OTHER: "YOU CAN SUBMIT A NEW REQUEST",
|
||||||
# # Assert that no other emails are sent on OTHER
|
}
|
||||||
# other = DomainRequest.ActionNeededReasons.OTHER
|
for i, (reason, email_content) in enumerate(expected_emails.items()):
|
||||||
# self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=other)
|
with self.subTest(reason=reason):
|
||||||
|
self.transition_state_and_send_email(domain_request, status=rejected, rejection_reason=reason)
|
||||||
# # Should be unchanged from before
|
self.assert_email_is_accurate(email_content, i, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||||
# self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), i + 1)
|
||||||
|
domain_request.rejection_reason_email = None
|
||||||
|
domain_request.save()
|
||||||
|
domain_request.refresh_from_db()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_save_model_sends_submitted_email(self):
|
def test_save_model_sends_submitted_email(self):
|
||||||
|
@ -1034,7 +1047,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
|
|
||||||
# Reject for reason REQUESTOR and test email including dynamic organization name
|
# Reject for reason REQUESTOR and test email including dynamic organization name
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request, DomainRequest.DomainRequestStatus.REJECTED, DomainRequest.RejectionReasons.REQUESTOR
|
domain_request,
|
||||||
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
DomainRequest.RejectionReasons.REQUESTOR_NOT_ELIGIBLE,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
||||||
|
@ -1072,7 +1087,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING,
|
DomainRequest.RejectionReasons.ORG_HAS_DOMAIN,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because Testorg has a .gov domain.", 0, _creator.email
|
"Your domain request was rejected because Testorg has a .gov domain.", 0, _creator.email
|
||||||
|
@ -1108,7 +1123,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
|
DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because we could not verify the organizational \n"
|
"Your domain request was rejected because we could not verify the organizational \n"
|
||||||
|
@ -1146,7 +1161,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(
|
self.transition_state_and_send_email(
|
||||||
domain_request,
|
domain_request,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.ORGANIZATION_ELIGIBILITY,
|
DomainRequest.RejectionReasons.ORG_NOT_ELIGIBLE,
|
||||||
)
|
)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
||||||
|
@ -1275,7 +1290,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
stack.enter_context(patch.object(messages, "error"))
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
stack.enter_context(patch.object(messages, "warning"))
|
stack.enter_context(patch.object(messages, "warning"))
|
||||||
domain_request.status = DomainRequest.DomainRequestStatus.REJECTED
|
domain_request.status = DomainRequest.DomainRequestStatus.REJECTED
|
||||||
domain_request.rejection_reason = DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY
|
domain_request.rejection_reason = DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED
|
||||||
|
|
||||||
self.admin.save_model(request, domain_request, None, True)
|
self.admin.save_model(request, domain_request, None, True)
|
||||||
|
|
||||||
|
@ -1511,7 +1526,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard", count=4)
|
self.assertContains(response, "copy-to-clipboard", count=4)
|
||||||
|
|
||||||
# Test that Creator counts display properly
|
# Test that Creator counts display properly
|
||||||
self.assertNotContains(response, "Approved domains")
|
self.assertNotContains(response, "Approved domains")
|
||||||
|
@ -1621,6 +1636,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
|
"rejection_reason_email",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
"action_needed_reason_email",
|
"action_needed_reason_email",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
@ -1840,12 +1856,64 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.trigger_saving_approved_to_another_state(
|
self.trigger_saving_approved_to_another_state(
|
||||||
False,
|
False,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
|
DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_side_effects_when_saving_approved_to_ineligible(self):
|
def test_side_effects_when_saving_approved_to_ineligible(self):
|
||||||
self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE)
|
self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE)
|
||||||
|
|
||||||
|
@less_console_noise
|
||||||
|
def test_error_when_saving_to_approved_and_domain_exists(self):
|
||||||
|
"""Redundant admin check on model transition not allowed."""
|
||||||
|
Domain.objects.create(name="wabbitseason.gov")
|
||||||
|
|
||||||
|
new_request = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.SUBMITTED, name="wabbitseason.gov"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk))
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
request.session = {}
|
||||||
|
|
||||||
|
# Use ExitStack to combine patch contexts
|
||||||
|
with ExitStack() as stack:
|
||||||
|
# Patch django.contrib.messages.error
|
||||||
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
|
|
||||||
|
new_request.status = DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
|
||||||
|
self.admin.save_model(request, new_request, None, True)
|
||||||
|
|
||||||
|
messages.error.assert_called_once_with(
|
||||||
|
request,
|
||||||
|
"Cannot approve. Requested domain is already in use.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise
|
||||||
|
def test_no_error_when_saving_to_approved_and_domain_exists(self):
|
||||||
|
"""The negative of the redundant admin check on model transition not allowed."""
|
||||||
|
new_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||||
|
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk))
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
request.session = {}
|
||||||
|
|
||||||
|
# Use ExitStack to combine patch contexts
|
||||||
|
with ExitStack() as stack:
|
||||||
|
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
||||||
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
|
|
||||||
|
new_request.status = DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
|
||||||
|
self.admin.save_model(request, new_request, None, True)
|
||||||
|
|
||||||
|
# Assert that the error message was never called
|
||||||
|
messages.error.assert_not_called()
|
||||||
|
|
||||||
def test_has_correct_filters(self):
|
def test_has_correct_filters(self):
|
||||||
"""
|
"""
|
||||||
This test verifies that DomainRequestAdmin has the correct filters set up.
|
This test verifies that DomainRequestAdmin has the correct filters set up.
|
||||||
|
|
|
@ -143,8 +143,8 @@ class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.assertIn("action_needed_email", data)
|
self.assertIn("email", data)
|
||||||
self.assertIn("ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
|
self.assertIn("ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", data["email"])
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_get_action_needed_email_for_user_json_analyst(self):
|
def test_get_action_needed_email_for_user_json_analyst(self):
|
||||||
|
@ -160,8 +160,8 @@ class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.assertIn("action_needed_email", data)
|
self.assertIn("email", data)
|
||||||
self.assertIn("SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
|
self.assertIn("SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", data["email"])
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_get_action_needed_email_for_user_json_regular(self):
|
def test_get_action_needed_email_for_user_json_regular(self):
|
||||||
|
@ -176,3 +176,71 @@ class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
|
||||||
|
class GetRejectionEmailForUserJsonTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.analyst_user = create_user()
|
||||||
|
self.agency = FederalAgency.objects.create(agency="Test Agency")
|
||||||
|
self.domain_request = completed_domain_request(
|
||||||
|
federal_agency=self.agency,
|
||||||
|
name="test.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.api_url = reverse("get-rejection-email-for-user-json")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
FederalAgency.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_rejected_email_for_user_json_superuser(self):
|
||||||
|
"""Test that a superuser can fetch the action needed email."""
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
self.api_url,
|
||||||
|
{
|
||||||
|
"reason": DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
|
"domain_request_id": self.domain_request.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("email", data)
|
||||||
|
self.assertIn("we could not verify the organizational", data["email"])
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_rejected_email_for_user_json_analyst(self):
|
||||||
|
"""Test that an analyst can fetch the action needed email."""
|
||||||
|
self.client.force_login(self.analyst_user)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
self.api_url,
|
||||||
|
{
|
||||||
|
"reason": DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
|
"domain_request_id": self.domain_request.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("email", data)
|
||||||
|
self.assertIn("we could not verify the organizational", data["email"])
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_get_rejected_email_for_user_json_regular(self):
|
||||||
|
"""Test that a regular user receives a 403 with an error message."""
|
||||||
|
p = "password"
|
||||||
|
self.client.login(username="testuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
self.api_url,
|
||||||
|
{
|
||||||
|
"reason": DomainRequest.RejectionReasons.CONTACTS_NOT_VERIFIED,
|
||||||
|
"domain_request_id": self.domain_request.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
1047
src/registrar/tests/test_models_requests.py
Normal file
1047
src/registrar/tests/test_models_requests.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -43,7 +43,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
|
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
self.TITLES = DomainRequestWizard.TITLES
|
self.TITLES = DomainRequestWizard.REGULAR_TITLES
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
@ -82,7 +82,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
response = self.app.get(f"/domain-request/{domain_request.id}")
|
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
# Ensure that the date is still set to None
|
# Ensure that the date is still set to None
|
||||||
self.assertIsNone(domain_request.last_status_update)
|
self.assertIsNone(domain_request.last_status_update)
|
||||||
print(response)
|
|
||||||
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
||||||
self.assertContains(response, "Started on:")
|
self.assertContains(response, "Started on:")
|
||||||
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))
|
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))
|
||||||
|
@ -2742,6 +2741,66 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
self.assertContains(review_page, "toggle-submit-domain-request")
|
self.assertContains(review_page, "toggle-submit-domain-request")
|
||||||
self.assertContains(review_page, "Your request form is incomplete")
|
self.assertContains(review_page, "Your request form is incomplete")
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_portfolio_user_missing_edit_permissions(self):
|
||||||
|
"""Tests that a portfolio user without edit request permissions cannot edit or add new requests"""
|
||||||
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
||||||
|
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
)
|
||||||
|
# This user should be forbidden from creating new domain requests
|
||||||
|
intro_page = self.app.get(reverse("domain-request:"), expect_errors=True)
|
||||||
|
self.assertEqual(intro_page.status_code, 403)
|
||||||
|
|
||||||
|
# This user should also be forbidden from editing existing ones
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}), expect_errors=True)
|
||||||
|
self.assertEqual(edit_page.status_code, 403)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
portfolio_perm.delete()
|
||||||
|
portfolio.delete()
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_portfolio_user_with_edit_permissions(self):
|
||||||
|
"""Tests that a portfolio user with edit request permissions can edit and add new requests"""
|
||||||
|
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
||||||
|
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
# This user should be allowed to create new domain requests
|
||||||
|
intro_page = self.app.get(reverse("domain-request:"))
|
||||||
|
self.assertEqual(intro_page.status_code, 200)
|
||||||
|
|
||||||
|
# This user should also be allowed to edit existing ones
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
||||||
|
self.assertEqual(edit_page.status_code, 200)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
portfolio_perm.delete()
|
||||||
|
portfolio.delete()
|
||||||
|
|
||||||
|
def test_non_creator_access(self):
|
||||||
|
"""Tests that a user cannot edit a domain request they didn't create"""
|
||||||
|
p = "password"
|
||||||
|
other_user = User.objects.create_user(username="other_user", password=p)
|
||||||
|
domain_request = completed_domain_request(user=other_user)
|
||||||
|
|
||||||
|
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}), expect_errors=True)
|
||||||
|
self.assertEqual(edit_page.status_code, 403)
|
||||||
|
|
||||||
|
def test_creator_access(self):
|
||||||
|
"""Tests that a user can edit a domain request they created"""
|
||||||
|
domain_request = completed_domain_request(user=self.user)
|
||||||
|
|
||||||
|
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
|
||||||
|
self.assertEqual(edit_page.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -2904,7 +2963,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||||
self.assertNotContains(home_page, "city.gov")
|
self.assertNotContains(home_page, "city.gov")
|
||||||
|
|
||||||
|
|
||||||
class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
class TestDomainRequestWizard(TestWithUser, WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
@ -3026,6 +3085,94 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
else:
|
else:
|
||||||
self.fail(f"Expected a redirect, but got a different response: {response}")
|
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_wizard_steps_portfolio(self):
|
||||||
|
"""
|
||||||
|
Tests the behavior of the domain request wizard for portfolios.
|
||||||
|
Ensures that:
|
||||||
|
- The user can access the organization page.
|
||||||
|
- The expected number of steps are locked/unlocked (implicit test for expected steps).
|
||||||
|
- The user lands on the "Requesting entity" page
|
||||||
|
- The user does not see the Domain and Domain requests buttons
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This should unlock 4 steps by default.
|
||||||
|
# Purpose, .gov domain, current websites, and requirements for operating
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
domain_request.anything_else = None
|
||||||
|
domain_request.save()
|
||||||
|
|
||||||
|
federal_agency = FederalAgency.objects.get(agency="Non-Federal Agency")
|
||||||
|
# Add a portfolio
|
||||||
|
portfolio = Portfolio.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
organization_name="test portfolio",
|
||||||
|
federal_agency=federal_agency,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_portfolio_permission = UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Check if the response is a redirect
|
||||||
|
if response.status_code == 302:
|
||||||
|
# Follow the redirect manually
|
||||||
|
try:
|
||||||
|
detail_page = response.follow()
|
||||||
|
|
||||||
|
self.wizard.get_context_data()
|
||||||
|
except Exception as err:
|
||||||
|
# Handle any potential errors while following the redirect
|
||||||
|
self.fail(f"Error following the redirect {err}")
|
||||||
|
|
||||||
|
# Now 'detail_page' contains the response after following the redirect
|
||||||
|
self.assertEqual(detail_page.status_code, 200)
|
||||||
|
|
||||||
|
# Assert that we're on the organization page
|
||||||
|
self.assertContains(detail_page, portfolio.organization_name)
|
||||||
|
|
||||||
|
# We should only see one unlocked step
|
||||||
|
self.assertContains(detail_page, "#check_circle", count=4)
|
||||||
|
|
||||||
|
# One pages should still be locked (additional details)
|
||||||
|
self.assertContains(detail_page, "#lock", 1)
|
||||||
|
|
||||||
|
# The current option should be selected
|
||||||
|
self.assertContains(detail_page, "usa-current", count=1)
|
||||||
|
|
||||||
|
# We default to the requesting entity page
|
||||||
|
expected_url = reverse("domain-request:portfolio_requesting_entity")
|
||||||
|
# This returns the entire url, thus "in"
|
||||||
|
self.assertIn(expected_url, detail_page.request.url)
|
||||||
|
|
||||||
|
# We shouldn't show the "domains" and "domain requests" buttons
|
||||||
|
# on this page.
|
||||||
|
self.assertNotContains(detail_page, "Domains")
|
||||||
|
self.assertNotContains(detail_page, "Domain requests")
|
||||||
|
else:
|
||||||
|
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||||
|
|
||||||
|
# Data cleanup
|
||||||
|
user_portfolio_permission.delete()
|
||||||
|
portfolio.delete()
|
||||||
|
federal_agency.delete()
|
||||||
|
domain_request.delete()
|
||||||
|
|
||||||
|
|
||||||
class TestPortfolioDomainRequestViewonly(TestWithUser, WebTest):
|
class TestPortfolioDomainRequestViewonly(TestWithUser, WebTest):
|
||||||
|
|
||||||
|
@ -3037,7 +3184,7 @@ class TestPortfolioDomainRequestViewonly(TestWithUser, WebTest):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
|
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
self.TITLES = DomainRequestWizard.TITLES
|
self.TITLES = DomainRequestWizard.REGULAR_TITLES
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
|
@ -6,36 +6,39 @@ from django.utils.html import escape
|
||||||
from registrar.models.utility.generic_helper import value_of_attribute
|
from registrar.models.utility.generic_helper import value_of_attribute
|
||||||
|
|
||||||
|
|
||||||
def get_all_action_needed_reason_emails(request, domain_request):
|
def get_action_needed_reason_default_email(domain_request, action_needed_reason):
|
||||||
"""Returns a dictionary of every action needed reason and its associated email
|
|
||||||
for this particular domain request."""
|
|
||||||
|
|
||||||
emails = {}
|
|
||||||
for action_needed_reason in domain_request.ActionNeededReasons:
|
|
||||||
# Map the action_needed_reason to its default email
|
|
||||||
emails[action_needed_reason.value] = get_action_needed_reason_default_email(
|
|
||||||
request, domain_request, action_needed_reason.value
|
|
||||||
)
|
|
||||||
|
|
||||||
return emails
|
|
||||||
|
|
||||||
|
|
||||||
def get_action_needed_reason_default_email(request, domain_request, action_needed_reason):
|
|
||||||
"""Returns the default email associated with the given action needed reason"""
|
"""Returns the default email associated with the given action needed reason"""
|
||||||
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
|
return _get_default_email(
|
||||||
|
domain_request,
|
||||||
|
file_path=f"emails/action_needed_reasons/{action_needed_reason}.txt",
|
||||||
|
reason=action_needed_reason,
|
||||||
|
excluded_reasons=[DomainRequest.ActionNeededReasons.OTHER],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rejection_reason_default_email(domain_request, rejection_reason):
|
||||||
|
"""Returns the default email associated with the given rejection reason"""
|
||||||
|
return _get_default_email(
|
||||||
|
domain_request,
|
||||||
|
file_path="emails/status_change_rejected.txt",
|
||||||
|
reason=rejection_reason,
|
||||||
|
# excluded_reasons=[DomainRequest.RejectionReasons.OTHER]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_email(domain_request, file_path, reason, excluded_reasons=None):
|
||||||
|
if not reason:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if excluded_reasons and reason in excluded_reasons:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
recipient = domain_request.creator
|
recipient = domain_request.creator
|
||||||
# Return the context of the rendered views
|
# Return the context of the rendered views
|
||||||
context = {"domain_request": domain_request, "recipient": recipient}
|
context = {"domain_request": domain_request, "recipient": recipient, "reason": reason}
|
||||||
|
|
||||||
# Get the email body
|
email_body_text = get_template(file_path).render(context=context)
|
||||||
template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt"
|
email_body_text_cleaned = email_body_text.strip().lstrip("\n") if email_body_text else None
|
||||||
|
|
||||||
email_body_text = get_template(template_path).render(context=context)
|
|
||||||
email_body_text_cleaned = None
|
|
||||||
if email_body_text:
|
|
||||||
email_body_text_cleaned = email_body_text.strip().lstrip("\n")
|
|
||||||
|
|
||||||
return email_body_text_cleaned
|
return email_body_text_cleaned
|
||||||
|
|
||||||
|
|
|
@ -74,10 +74,13 @@ class PortfolioDomainRequestStep(StrEnum):
|
||||||
appear in the order they are defined. (Order matters.)
|
appear in the order they are defined. (Order matters.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Portfolio
|
# NOTE: Append portfolio_ when customizing a view for portfolio.
|
||||||
REQUESTING_ENTITY = "organization_name"
|
# By default, these will redirect to the normal request flow views.
|
||||||
|
# After creating a new view, you will need to add this to urls.py.
|
||||||
|
REQUESTING_ENTITY = "portfolio_requesting_entity"
|
||||||
CURRENT_SITES = "current_sites"
|
CURRENT_SITES = "current_sites"
|
||||||
DOTGOV_DOMAIN = "dotgov_domain"
|
DOTGOV_DOMAIN = "dotgov_domain"
|
||||||
PURPOSE = "purpose"
|
PURPOSE = "purpose"
|
||||||
ADDITIONAL_DETAILS = "additional_details"
|
ADDITIONAL_DETAILS = "portfolio_additional_details"
|
||||||
REQUIREMENTS = "requirements"
|
REQUIREMENTS = "requirements"
|
||||||
|
REVIEW = "review"
|
||||||
|
|
|
@ -43,8 +43,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
although not without consulting the base implementation, first.
|
although not without consulting the base implementation, first.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
StepEnum: Step = Step # type: ignore
|
|
||||||
template_name = ""
|
template_name = ""
|
||||||
|
is_portfolio = False
|
||||||
|
|
||||||
# uniquely namespace the wizard in urls.py
|
# uniquely namespace the wizard in urls.py
|
||||||
# (this is not seen _in_ urls, only for Django's internal naming)
|
# (this is not seen _in_ urls, only for Django's internal naming)
|
||||||
|
@ -54,42 +54,140 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
# name for accessing /domain-request/<id>/edit
|
# name for accessing /domain-request/<id>/edit
|
||||||
EDIT_URL_NAME = "edit-domain-request"
|
EDIT_URL_NAME = "edit-domain-request"
|
||||||
NEW_URL_NAME = "/request/"
|
NEW_URL_NAME = "/request/"
|
||||||
|
|
||||||
|
# region: Titles
|
||||||
# We need to pass our human-readable step titles as context to the templates.
|
# We need to pass our human-readable step titles as context to the templates.
|
||||||
TITLES = {
|
REGULAR_TITLES = {
|
||||||
StepEnum.ORGANIZATION_TYPE: _("Type of organization"),
|
Step.ORGANIZATION_TYPE: _("Type of organization"),
|
||||||
StepEnum.TRIBAL_GOVERNMENT: _("Tribal government"),
|
Step.TRIBAL_GOVERNMENT: _("Tribal government"),
|
||||||
StepEnum.ORGANIZATION_FEDERAL: _("Federal government branch"),
|
Step.ORGANIZATION_FEDERAL: _("Federal government branch"),
|
||||||
StepEnum.ORGANIZATION_ELECTION: _("Election office"),
|
Step.ORGANIZATION_ELECTION: _("Election office"),
|
||||||
StepEnum.ORGANIZATION_CONTACT: _("Organization"),
|
Step.ORGANIZATION_CONTACT: _("Organization"),
|
||||||
StepEnum.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
||||||
StepEnum.SENIOR_OFFICIAL: _("Senior official"),
|
Step.SENIOR_OFFICIAL: _("Senior official"),
|
||||||
StepEnum.CURRENT_SITES: _("Current websites"),
|
Step.CURRENT_SITES: _("Current websites"),
|
||||||
StepEnum.DOTGOV_DOMAIN: _(".gov domain"),
|
Step.DOTGOV_DOMAIN: _(".gov domain"),
|
||||||
StepEnum.PURPOSE: _("Purpose of your domain"),
|
Step.PURPOSE: _("Purpose of your domain"),
|
||||||
StepEnum.OTHER_CONTACTS: _("Other employees from your organization"),
|
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||||
StepEnum.ADDITIONAL_DETAILS: _("Additional details"),
|
Step.ADDITIONAL_DETAILS: _("Additional details"),
|
||||||
StepEnum.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||||
StepEnum.REVIEW: _("Review and submit your domain request"),
|
Step.REVIEW: _("Review and submit your domain request"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Titles for the portfolio context
|
||||||
|
PORTFOLIO_TITLES = {
|
||||||
|
PortfolioDomainRequestStep.REQUESTING_ENTITY: _("Requesting entity"),
|
||||||
|
PortfolioDomainRequestStep.CURRENT_SITES: _("Current websites"),
|
||||||
|
PortfolioDomainRequestStep.DOTGOV_DOMAIN: _(".gov domain"),
|
||||||
|
PortfolioDomainRequestStep.PURPOSE: _("Purpose of your domain"),
|
||||||
|
PortfolioDomainRequestStep.ADDITIONAL_DETAILS: _("Additional details"),
|
||||||
|
PortfolioDomainRequestStep.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||||
|
PortfolioDomainRequestStep.REVIEW: _("Review and submit your domain request"),
|
||||||
|
}
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region: Wizard conditions
|
||||||
# We can use a dictionary with step names and callables that return booleans
|
# We can use a dictionary with step names and callables that return booleans
|
||||||
# to show or hide particular steps based on the state of the process.
|
# to show or hide particular steps based on the state of the process.
|
||||||
WIZARD_CONDITIONS = {
|
REGULAR_WIZARD_CONDITIONS = {
|
||||||
StepEnum.ORGANIZATION_FEDERAL: lambda w: w.from_model("show_organization_federal", False),
|
Step.ORGANIZATION_FEDERAL: lambda w: w.from_model("show_organization_federal", False),
|
||||||
StepEnum.TRIBAL_GOVERNMENT: lambda w: w.from_model("show_tribal_government", False),
|
Step.TRIBAL_GOVERNMENT: lambda w: w.from_model("show_tribal_government", False),
|
||||||
StepEnum.ORGANIZATION_ELECTION: lambda w: w.from_model("show_organization_election", False),
|
Step.ORGANIZATION_ELECTION: lambda w: w.from_model("show_organization_election", False),
|
||||||
StepEnum.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model("show_about_your_organization", False),
|
Step.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model("show_about_your_organization", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PORTFOLIO_WIZARD_CONDITIONS = {} # type: ignore
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region: Unlocking steps
|
||||||
|
# The conditions by which each step is "unlocked" or "locked"
|
||||||
|
REGULAR_UNLOCKING_STEPS = {
|
||||||
|
Step.ORGANIZATION_TYPE: lambda self: self.domain_request.generic_org_type is not None,
|
||||||
|
Step.TRIBAL_GOVERNMENT: lambda self: self.domain_request.tribe_name is not None,
|
||||||
|
Step.ORGANIZATION_FEDERAL: lambda self: self.domain_request.federal_type is not None,
|
||||||
|
Step.ORGANIZATION_ELECTION: lambda self: self.domain_request.is_election_board is not None,
|
||||||
|
Step.ORGANIZATION_CONTACT: lambda self: (
|
||||||
|
self.domain_request.federal_agency is not None
|
||||||
|
or self.domain_request.organization_name is not None
|
||||||
|
or self.domain_request.address_line1 is not None
|
||||||
|
or self.domain_request.city is not None
|
||||||
|
or self.domain_request.state_territory is not None
|
||||||
|
or self.domain_request.zipcode is not None
|
||||||
|
or self.domain_request.urbanization is not None
|
||||||
|
),
|
||||||
|
Step.ABOUT_YOUR_ORGANIZATION: lambda self: self.domain_request.about_your_organization is not None,
|
||||||
|
Step.SENIOR_OFFICIAL: lambda self: self.domain_request.senior_official is not None,
|
||||||
|
Step.CURRENT_SITES: lambda self: (
|
||||||
|
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
|
||||||
|
),
|
||||||
|
Step.DOTGOV_DOMAIN: lambda self: self.domain_request.requested_domain is not None,
|
||||||
|
Step.PURPOSE: lambda self: self.domain_request.purpose is not None,
|
||||||
|
Step.OTHER_CONTACTS: lambda self: (
|
||||||
|
self.domain_request.other_contacts.exists() or self.domain_request.no_other_contacts_rationale is not None
|
||||||
|
),
|
||||||
|
Step.ADDITIONAL_DETAILS: lambda self: (
|
||||||
|
# Additional details is complete as long as "has anything else" and "has cisa rep" are not None
|
||||||
|
(
|
||||||
|
self.domain_request.has_anything_else_text is not None
|
||||||
|
and self.domain_request.has_cisa_representative is not None
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Step.REQUIREMENTS: lambda self: self.domain_request.is_policy_acknowledged is not None,
|
||||||
|
Step.REVIEW: lambda self: self.domain_request.is_policy_acknowledged is not None,
|
||||||
|
}
|
||||||
|
|
||||||
|
PORTFOLIO_UNLOCKING_STEPS = {
|
||||||
|
PortfolioDomainRequestStep.REQUESTING_ENTITY: lambda self: self.domain_request.organization_name is not None,
|
||||||
|
PortfolioDomainRequestStep.CURRENT_SITES: lambda self: (
|
||||||
|
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
|
||||||
|
),
|
||||||
|
PortfolioDomainRequestStep.DOTGOV_DOMAIN: lambda self: self.domain_request.requested_domain is not None,
|
||||||
|
PortfolioDomainRequestStep.PURPOSE: lambda self: self.domain_request.purpose is not None,
|
||||||
|
PortfolioDomainRequestStep.ADDITIONAL_DETAILS: lambda self: self.domain_request.anything_else is not None,
|
||||||
|
PortfolioDomainRequestStep.REQUIREMENTS: lambda self: self.domain_request.is_policy_acknowledged is not None,
|
||||||
|
PortfolioDomainRequestStep.REVIEW: lambda self: self.domain_request.is_policy_acknowledged is not None,
|
||||||
|
}
|
||||||
|
# endregion
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.steps = StepsHelper(self)
|
self.titles = {}
|
||||||
|
self.wizard_conditions = {}
|
||||||
|
self.unlocking_steps = {}
|
||||||
|
self.steps = None
|
||||||
|
# Configure titles, wizard_conditions, unlocking_steps, and steps
|
||||||
|
self.configure_step_options()
|
||||||
self._domain_request = None # for caching
|
self._domain_request = None # for caching
|
||||||
|
|
||||||
|
def configure_step_options(self):
|
||||||
|
"""Changes which steps are available to the user based on self.is_portfolio.
|
||||||
|
This may change on the fly, so we need to evaluate it on the fly.
|
||||||
|
|
||||||
|
Using this information, we then set three configuration variables.
|
||||||
|
- self.titles => Returns the page titles for each step
|
||||||
|
- self.wizard_conditions => Conditionally shows / hides certain steps
|
||||||
|
- self.unlocking_steps => Determines what steps are locked/unlocked
|
||||||
|
|
||||||
|
Then, we create self.steps.
|
||||||
|
"""
|
||||||
|
if self.is_portfolio:
|
||||||
|
self.titles = self.PORTFOLIO_TITLES
|
||||||
|
self.wizard_conditions = self.PORTFOLIO_WIZARD_CONDITIONS
|
||||||
|
self.unlocking_steps = self.PORTFOLIO_UNLOCKING_STEPS
|
||||||
|
else:
|
||||||
|
self.titles = self.REGULAR_TITLES
|
||||||
|
self.wizard_conditions = self.REGULAR_WIZARD_CONDITIONS
|
||||||
|
self.unlocking_steps = self.REGULAR_UNLOCKING_STEPS
|
||||||
|
self.steps = StepsHelper(self)
|
||||||
|
|
||||||
def has_pk(self):
|
def has_pk(self):
|
||||||
"""Does this wizard know about a DomainRequest database record?"""
|
"""Does this wizard know about a DomainRequest database record?"""
|
||||||
return "domain_request_id" in self.storage
|
return "domain_request_id" in self.storage
|
||||||
|
|
||||||
|
def get_step_enum(self):
|
||||||
|
"""Determines which step enum we should use for the wizard"""
|
||||||
|
return PortfolioDomainRequestStep if self.is_portfolio else Step
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prefix(self):
|
def prefix(self):
|
||||||
"""Namespace the wizard to avoid clashes in session variable names."""
|
"""Namespace the wizard to avoid clashes in session variable names."""
|
||||||
|
@ -128,10 +226,16 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
# If a user is creating a request, we assume that perms are handled upstream
|
# If a user is creating a request, we assume that perms are handled upstream
|
||||||
if self.request.user.is_org_user(self.request):
|
if self.request.user.is_org_user(self.request):
|
||||||
|
portfolio = self.request.session.get("portfolio")
|
||||||
self._domain_request = DomainRequest.objects.create(
|
self._domain_request = DomainRequest.objects.create(
|
||||||
creator=self.request.user,
|
creator=self.request.user,
|
||||||
portfolio=self.request.session.get("portfolio"),
|
portfolio=portfolio,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Question for reviewers: we should probably be doing this right?
|
||||||
|
if portfolio and not self._domain_request.generic_org_type:
|
||||||
|
self._domain_request.generic_org_type = portfolio.organization_type
|
||||||
|
self._domain_request.save()
|
||||||
else:
|
else:
|
||||||
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
||||||
|
|
||||||
|
@ -190,6 +294,12 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""This method handles GET requests."""
|
"""This method handles GET requests."""
|
||||||
|
|
||||||
|
if not self.is_portfolio and self.request.user.is_org_user(request):
|
||||||
|
self.is_portfolio = True
|
||||||
|
# Configure titles, wizard_conditions, unlocking_steps, and steps
|
||||||
|
self.configure_step_options()
|
||||||
|
|
||||||
current_url = resolve(request.path_info).url_name
|
current_url = resolve(request.path_info).url_name
|
||||||
|
|
||||||
# if user visited via an "edit" url, associate the id of the
|
# if user visited via an "edit" url, associate the id of the
|
||||||
|
@ -209,7 +319,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
# Clear context so the prop getter won't create a request here.
|
# Clear context so the prop getter won't create a request here.
|
||||||
# Creating a request will be handled in the post method for the
|
# Creating a request will be handled in the post method for the
|
||||||
# intro page.
|
# intro page.
|
||||||
return render(request, "domain_request_intro.html", {})
|
return render(request, "domain_request_intro.html", {"hide_requests": True, "hide_domains": True})
|
||||||
else:
|
else:
|
||||||
return self.goto(self.steps.first)
|
return self.goto(self.steps.first)
|
||||||
|
|
||||||
|
@ -321,56 +431,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
return DomainRequest.objects.filter(creator=self.request.user, status__in=check_statuses)
|
return DomainRequest.objects.filter(creator=self.request.user, status__in=check_statuses)
|
||||||
|
|
||||||
def db_check_for_unlocking_steps(self):
|
def db_check_for_unlocking_steps(self):
|
||||||
"""Helper for get_context_data
|
"""Helper for get_context_data.
|
||||||
|
|
||||||
Queries the DB for a domain request and returns a list of unlocked steps."""
|
Queries the DB for a domain request and returns a list of unlocked steps."""
|
||||||
|
return [key for key, is_unlocked_checker in self.unlocking_steps.items() if is_unlocked_checker(self)]
|
||||||
# The way this works is as follows:
|
|
||||||
# Each step is assigned a true/false value to determine if it is
|
|
||||||
# "unlocked" or not. This dictionary of values is looped through
|
|
||||||
# at the end of this function and any step with a "true" value is
|
|
||||||
# added to a simple array that is returned at the end of this function.
|
|
||||||
# This array is eventually passed to the frontend context (eg. domain_request_sidebar.html),
|
|
||||||
# and is used to determine how steps appear in the side nav.
|
|
||||||
# It is worth noting that any step assigned "false" here will be EXCLUDED
|
|
||||||
# from the list of "unlocked" steps.
|
|
||||||
|
|
||||||
history_dict = {
|
|
||||||
"generic_org_type": self.domain_request.generic_org_type is not None,
|
|
||||||
"tribal_government": self.domain_request.tribe_name is not None,
|
|
||||||
"organization_federal": self.domain_request.federal_type is not None,
|
|
||||||
"organization_election": self.domain_request.is_election_board is not None,
|
|
||||||
"organization_contact": (
|
|
||||||
self.domain_request.federal_agency is not None
|
|
||||||
or self.domain_request.organization_name is not None
|
|
||||||
or self.domain_request.address_line1 is not None
|
|
||||||
or self.domain_request.city is not None
|
|
||||||
or self.domain_request.state_territory is not None
|
|
||||||
or self.domain_request.zipcode is not None
|
|
||||||
or self.domain_request.urbanization is not None
|
|
||||||
),
|
|
||||||
"about_your_organization": self.domain_request.about_your_organization is not None,
|
|
||||||
"senior_official": self.domain_request.senior_official is not None,
|
|
||||||
"current_sites": (
|
|
||||||
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
|
|
||||||
),
|
|
||||||
"dotgov_domain": self.domain_request.requested_domain is not None,
|
|
||||||
"purpose": self.domain_request.purpose is not None,
|
|
||||||
"other_contacts": (
|
|
||||||
self.domain_request.other_contacts.exists()
|
|
||||||
or self.domain_request.no_other_contacts_rationale is not None
|
|
||||||
),
|
|
||||||
"additional_details": (
|
|
||||||
# Additional details is complete as long as "has anything else" and "has cisa rep" are not None
|
|
||||||
(
|
|
||||||
self.domain_request.has_anything_else_text is not None
|
|
||||||
and self.domain_request.has_cisa_representative is not None
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
|
||||||
"review": self.domain_request.is_policy_acknowledged is not None,
|
|
||||||
}
|
|
||||||
return [key for key, value in history_dict.items() if value]
|
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
"""Define context for access on all wizard pages."""
|
"""Define context for access on all wizard pages."""
|
||||||
|
@ -380,11 +443,16 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
requested_domain_name = self.domain_request.requested_domain.name
|
requested_domain_name = self.domain_request.requested_domain.name
|
||||||
|
|
||||||
context_stuff = {}
|
context_stuff = {}
|
||||||
if DomainRequest._form_complete(self.domain_request, self.request):
|
|
||||||
|
# Note: we will want to consolidate the non_org_steps_complete check into the same check that
|
||||||
|
# org_steps_complete is using at some point.
|
||||||
|
non_org_steps_complete = DomainRequest._form_complete(self.domain_request, self.request)
|
||||||
|
org_steps_complete = len(self.db_check_for_unlocking_steps()) == len(self.steps)
|
||||||
|
if (not self.is_portfolio and non_org_steps_complete) or (self.is_portfolio and org_steps_complete):
|
||||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
||||||
context_stuff = {
|
context_stuff = {
|
||||||
"not_form": False,
|
"not_form": False,
|
||||||
"form_titles": self.TITLES,
|
"form_titles": self.titles,
|
||||||
"steps": self.steps,
|
"steps": self.steps,
|
||||||
"visited": self.storage.get("step_history", []),
|
"visited": self.storage.get("step_history", []),
|
||||||
"is_federal": self.domain_request.is_federal(),
|
"is_federal": self.domain_request.is_federal(),
|
||||||
|
@ -401,7 +469,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
||||||
context_stuff = {
|
context_stuff = {
|
||||||
"not_form": True,
|
"not_form": True,
|
||||||
"form_titles": self.TITLES,
|
"form_titles": self.titles,
|
||||||
"steps": self.steps,
|
"steps": self.steps,
|
||||||
"visited": self.storage.get("step_history", []),
|
"visited": self.storage.get("step_history", []),
|
||||||
"is_federal": self.domain_request.is_federal(),
|
"is_federal": self.domain_request.is_federal(),
|
||||||
|
@ -413,14 +481,19 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
"requested_domain__name": requested_domain_name,
|
"requested_domain__name": requested_domain_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Hides the requests and domains buttons in the navbar
|
||||||
|
context_stuff["hide_requests"] = self.is_portfolio
|
||||||
|
context_stuff["hide_domains"] = self.is_portfolio
|
||||||
|
|
||||||
return context_stuff
|
return context_stuff
|
||||||
|
|
||||||
def get_step_list(self) -> list:
|
def get_step_list(self) -> list:
|
||||||
"""Dynamically generated list of steps in the form wizard."""
|
"""Dynamically generated list of steps in the form wizard."""
|
||||||
return request_step_list(self)
|
return request_step_list(self, self.get_step_enum())
|
||||||
|
|
||||||
def goto(self, step):
|
def goto(self, step):
|
||||||
if step == "generic_org_type":
|
if step == "generic_org_type" or step == "portfolio_requesting_entity":
|
||||||
# We need to avoid creating a new domain request if the user
|
# We need to avoid creating a new domain request if the user
|
||||||
# clicks the back button
|
# clicks the back button
|
||||||
self.request.session["new_request"] = False
|
self.request.session["new_request"] = False
|
||||||
|
@ -443,21 +516,21 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
def post(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
"""This method handles POST requests."""
|
"""This method handles POST requests."""
|
||||||
|
if not self.is_portfolio and self.request.user.is_org_user(request): # type: ignore
|
||||||
|
self.is_portfolio = True
|
||||||
|
# Configure titles, wizard_conditions, unlocking_steps, and steps
|
||||||
|
self.configure_step_options()
|
||||||
|
|
||||||
# which button did the user press?
|
# which button did the user press?
|
||||||
button: str = request.POST.get("submit_button", "")
|
button: str = request.POST.get("submit_button", "")
|
||||||
# If a user hits the new request url directly
|
|
||||||
if "new_request" not in request.session:
|
if "new_request" not in request.session:
|
||||||
request.session["new_request"] = True
|
request.session["new_request"] = True
|
||||||
|
|
||||||
# if user has acknowledged the intro message
|
# if user has acknowledged the intro message
|
||||||
if button == "intro_acknowledge":
|
if button == "intro_acknowledge":
|
||||||
if request.path_info == self.NEW_URL_NAME:
|
# Split into a function: C901 'DomainRequestWizard.post' is too complex (11)
|
||||||
|
self.handle_intro_acknowledge(request)
|
||||||
if self.request.session["new_request"] is True:
|
|
||||||
# This will trigger the domain_request getter into creating a new DomainRequest
|
|
||||||
del self.storage
|
|
||||||
|
|
||||||
return self.goto(self.steps.first)
|
|
||||||
|
|
||||||
# if accessing this class directly, redirect to the first step
|
# if accessing this class directly, redirect to the first step
|
||||||
if self.__class__ == DomainRequestWizard:
|
if self.__class__ == DomainRequestWizard:
|
||||||
|
@ -488,6 +561,14 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
# otherwise, proceed as normal
|
# otherwise, proceed as normal
|
||||||
return self.goto_next_step()
|
return self.goto_next_step()
|
||||||
|
|
||||||
|
def handle_intro_acknowledge(self, request):
|
||||||
|
"""If we are starting a new request, clear storage
|
||||||
|
and redirect to the first step"""
|
||||||
|
if request.path_info == self.NEW_URL_NAME:
|
||||||
|
if self.request.session["new_request"] is True:
|
||||||
|
del self.storage
|
||||||
|
return self.goto(self.steps.first)
|
||||||
|
|
||||||
def save(self, forms: list):
|
def save(self, forms: list):
|
||||||
"""
|
"""
|
||||||
Unpack the form responses onto the model object properties.
|
Unpack the form responses onto the model object properties.
|
||||||
|
@ -501,24 +582,22 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
# TODO - this is a WIP until the domain request experience for portfolios is complete
|
# TODO - this is a WIP until the domain request experience for portfolios is complete
|
||||||
class PortfolioDomainRequestWizard(DomainRequestWizard):
|
class PortfolioDomainRequestWizard(DomainRequestWizard):
|
||||||
StepEnum: PortfolioDomainRequestStep = PortfolioDomainRequestStep # type: ignore
|
is_portfolio = True
|
||||||
|
|
||||||
TITLES = {
|
|
||||||
StepEnum.REQUESTING_ENTITY: _("Requesting entity"),
|
|
||||||
StepEnum.CURRENT_SITES: _("Current websites"),
|
|
||||||
StepEnum.DOTGOV_DOMAIN: _(".gov domain"),
|
|
||||||
StepEnum.PURPOSE: _("Purpose of your domain"),
|
|
||||||
StepEnum.ADDITIONAL_DETAILS: _("Additional details"),
|
|
||||||
StepEnum.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
|
||||||
# Step.REVIEW: _("Review and submit your domain request"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.steps = StepsHelper(self)
|
|
||||||
self._domain_request = None # for caching
|
|
||||||
|
|
||||||
|
|
||||||
|
# Portfolio pages
|
||||||
|
class RequestingEntity(DomainRequestWizard):
|
||||||
|
template_name = "domain_request_requesting_entity.html"
|
||||||
|
forms = [forms.RequestingEntityForm]
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioAdditionalDetails(DomainRequestWizard):
|
||||||
|
template_name = "portfolio_domain_request_additional_details.html"
|
||||||
|
|
||||||
|
forms = [forms.AnythingElseForm]
|
||||||
|
|
||||||
|
|
||||||
|
# Non-portfolio pages
|
||||||
class OrganizationType(DomainRequestWizard):
|
class OrganizationType(DomainRequestWizard):
|
||||||
template_name = "domain_request_org_type.html"
|
template_name = "domain_request_org_type.html"
|
||||||
forms = [forms.OrganizationTypeForm]
|
forms = [forms.OrganizationTypeForm]
|
||||||
|
@ -698,7 +777,7 @@ class Review(DomainRequestWizard):
|
||||||
if DomainRequest._form_complete(self.domain_request, self.request) is False:
|
if DomainRequest._form_complete(self.domain_request, self.request) is False:
|
||||||
logger.warning("User arrived at review page with an incomplete form.")
|
logger.warning("User arrived at review page with an incomplete form.")
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
context["Step"] = Step.__members__
|
context["Step"] = self.get_step_enum().__members__
|
||||||
context["domain_request"] = self.domain_request
|
context["domain_request"] = self.domain_request
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -899,9 +978,9 @@ class PortfolioDomainRequestStatusViewOnly(DomainRequestPortfolioViewonlyView):
|
||||||
# Create a temp wizard object to grab the step list
|
# Create a temp wizard object to grab the step list
|
||||||
wizard = PortfolioDomainRequestWizard()
|
wizard = PortfolioDomainRequestWizard()
|
||||||
wizard.request = self.request
|
wizard.request = self.request
|
||||||
context["Step"] = wizard.StepEnum.__members__
|
context["Step"] = PortfolioDomainRequestStep.__members__
|
||||||
context["steps"] = request_step_list(wizard)
|
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
|
||||||
context["form_titles"] = wizard.TITLES
|
context["form_titles"] = wizard.titles
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.forms.models import model_to_dict
|
||||||
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from registrar.utility.admin_helpers import get_all_action_needed_reason_emails
|
from registrar.utility.admin_helpers import get_action_needed_reason_default_email, get_rejection_reason_default_email
|
||||||
from registrar.models.portfolio import Portfolio
|
from registrar.models.portfolio import Portfolio
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
|
|
||||||
|
@ -88,5 +88,30 @@ def get_action_needed_email_for_user_json(request):
|
||||||
return JsonResponse({"error": "No domain_request_id specified"}, status=404)
|
return JsonResponse({"error": "No domain_request_id specified"}, status=404)
|
||||||
|
|
||||||
domain_request = DomainRequest.objects.filter(id=domain_request_id).first()
|
domain_request = DomainRequest.objects.filter(id=domain_request_id).first()
|
||||||
emails = get_all_action_needed_reason_emails(request, domain_request)
|
|
||||||
return JsonResponse({"action_needed_email": emails.get(reason)}, status=200)
|
email = get_action_needed_reason_default_email(domain_request, reason)
|
||||||
|
return JsonResponse({"email": email}, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@staff_member_required
|
||||||
|
def get_rejection_email_for_user_json(request):
|
||||||
|
"""Returns a default rejection email for a given user"""
|
||||||
|
|
||||||
|
# This API is only accessible to admins and analysts
|
||||||
|
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||||
|
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||||
|
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||||
|
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||||
|
|
||||||
|
reason = request.GET.get("reason")
|
||||||
|
domain_request_id = request.GET.get("domain_request_id")
|
||||||
|
if not reason:
|
||||||
|
return JsonResponse({"error": "No reason specified"}, status=404)
|
||||||
|
|
||||||
|
if not domain_request_id:
|
||||||
|
return JsonResponse({"error": "No domain_request_id specified"}, status=404)
|
||||||
|
|
||||||
|
domain_request = DomainRequest.objects.filter(id=domain_request_id).first()
|
||||||
|
email = get_rejection_reason_default_email(domain_request, reason)
|
||||||
|
return JsonResponse({"email": email}, status=200)
|
||||||
|
|
|
@ -384,10 +384,32 @@ class DomainRequestWizardPermission(PermissionsLoginMixin):
|
||||||
The user is in self.request.user
|
The user is in self.request.user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
# The user has an ineligible flag
|
# The user has an ineligible flag
|
||||||
if self.request.user.is_restricted():
|
if self.request.user.is_restricted():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# If the user is an org user and doesn't have add/edit perms, forbid this
|
||||||
|
if self.request.user.is_org_user(self.request):
|
||||||
|
portfolio = self.request.session.get("portfolio")
|
||||||
|
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# user needs to be the creator of the domain request to edit it.
|
||||||
|
id = self.kwargs.get("id") if hasattr(self, "kwargs") else None
|
||||||
|
if not id:
|
||||||
|
domain_request_wizard = self.request.session.get("wizard_domain_request")
|
||||||
|
if domain_request_wizard:
|
||||||
|
id = domain_request_wizard.get("domain_request_id")
|
||||||
|
|
||||||
|
# If no id is provided, we can assume that the user is starting a new request.
|
||||||
|
# If one IS provided, check that they are the original creator of it.
|
||||||
|
if id:
|
||||||
|
if not DomainRequest.objects.filter(creator=self.request.user, id=id).exists():
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue