diff --git a/.github/workflows/deploy-branch-to-sandbox.yaml b/.github/workflows/deploy-branch-to-sandbox.yaml
index f57961fa8..652aec207 100644
--- a/.github/workflows/deploy-branch-to-sandbox.yaml
+++ b/.github/workflows/deploy-branch-to-sandbox.yaml
@@ -29,6 +29,7 @@ on:
- hotgov
- litterbox
- ms
+ - ad
# GitHub Actions has no "good" way yet to dynamically input branches
branch:
description: 'Branch to deploy'
diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml
index 57561919c..fe0a19089 100644
--- a/.github/workflows/deploy-sandbox.yaml
+++ b/.github/workflows/deploy-sandbox.yaml
@@ -29,6 +29,7 @@ jobs:
|| startsWith(github.head_ref, 'litterbox/')
|| startsWith(github.head_ref, 'ag/')
|| startsWith(github.head_ref, 'ms/')
+ || startsWith(github.head_ref, 'ad/')
outputs:
environment: ${{ steps.var.outputs.environment}}
runs-on: "ubuntu-latest"
diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml
index 3ebee59f9..70ff8ee95 100644
--- a/.github/workflows/migrate.yaml
+++ b/.github/workflows/migrate.yaml
@@ -16,6 +16,7 @@ on:
- stable
- staging
- development
+ - ad
- ms
- ag
- litterbox
diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml
index 49e4b5e5f..b6fa0fec5 100644
--- a/.github/workflows/reset-db.yaml
+++ b/.github/workflows/reset-db.yaml
@@ -16,6 +16,7 @@ on:
options:
- staging
- development
+ - ad
- ms
- ag
- litterbox
diff --git a/ops/manifests/manifest-ad.yaml b/ops/manifests/manifest-ad.yaml
new file mode 100644
index 000000000..73d6f96ff
--- /dev/null
+++ b/ops/manifests/manifest-ad.yaml
@@ -0,0 +1,32 @@
+---
+applications:
+- name: getgov-ad
+ buildpacks:
+ - python_buildpack
+ path: ../../src
+ instances: 1
+ memory: 512M
+ stack: cflinuxfs4
+ timeout: 180
+ command: ./run.sh
+ health-check-type: http
+ health-check-http-endpoint: /health
+ health-check-invocation-timeout: 40
+ env:
+ # Send stdout and stderr straight to the terminal without buffering
+ PYTHONUNBUFFERED: yup
+ # Tell Django where to find its configuration
+ DJANGO_SETTINGS_MODULE: registrar.config.settings
+ # Tell Django where it is being hosted
+ DJANGO_BASE_URL: https://getgov-ad.app.cloud.gov
+ # Tell Django how much stuff to log
+ DJANGO_LOG_LEVEL: INFO
+ # default public site location
+ GETGOV_PUBLIC_SITE_URL: https://get.gov
+ # Flag to disable/enable features in prod environments
+ IS_PRODUCTION: False
+ routes:
+ - route: getgov-ad.app.cloud.gov
+ services:
+ - getgov-credentials
+ - getgov-ad-database
diff --git a/src/package-lock.json b/src/package-lock.json
index 2ff464d5e..08e70dd51 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
- "@uswds/uswds": "^3.8.0",
+ "@uswds/uswds": "^3.8.1",
"pa11y-ci": "^3.0.1",
"sass": "^1.54.8"
},
@@ -187,9 +187,10 @@
}
},
"node_modules/@uswds/uswds": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.8.0.tgz",
- "integrity": "sha512-rMwCXe/u4HGkfskvS1Iuabapi/EXku3ChaIFW7y/dUhc7R1TXQhbbfp8YXEjmXPF0yqJnv9T08WPgS0fQqWZ8w==",
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.8.1.tgz",
+ "integrity": "sha512-bKG/B9mJF1v0yoqth48wQDzST5Xyu3OxxpePIPDyhKWS84oDrCehnu3Z88JhSjdIAJMl8dtjtH8YvdO9kZUpAg==",
+ "license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"classlist-polyfill": "1.2.0",
"object-assign": "4.1.1",
diff --git a/src/package.json b/src/package.json
index 58ae3a4ed..e16bc8198 100644
--- a/src/package.json
+++ b/src/package.json
@@ -10,11 +10,11 @@
"author": "",
"license": "ISC",
"dependencies": {
- "@uswds/uswds": "^3.8.0",
+ "@uswds/uswds": "^3.8.1",
"pa11y-ci": "^3.0.1",
"sass": "^1.54.8"
},
"devDependencies": {
"@uswds/compile": "^1.0.0-beta.3"
}
-}
\ No newline at end of file
+}
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index d8bc21899..04f5417b0 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -207,15 +207,11 @@ function addOrRemoveSessionBoolean(name, add){
})();
+
/** An IIFE for pages in DjangoAdmin that use a clipboard button
*/
(function (){
- function copyInnerTextToClipboard(elem) {
- let text = elem.innerText
- navigator.clipboard.writeText(text)
- }
-
function copyToClipboardAndChangeIcon(button) {
// Assuming the input is the previous sibling of the button
let input = button.previousElementSibling;
@@ -224,7 +220,7 @@ function addOrRemoveSessionBoolean(name, add){
if (input) {
navigator.clipboard.writeText(input.value).then(function() {
// Change the icon to a checkmark on successful copy
- let buttonIcon = button.querySelector('.usa-button__clipboard use');
+ let buttonIcon = button.querySelector('.copy-to-clipboard use');
if (buttonIcon) {
let currentHref = buttonIcon.getAttribute('xlink:href');
let baseHref = currentHref.split('#')[0];
@@ -233,21 +229,17 @@ function addOrRemoveSessionBoolean(name, add){
buttonIcon.setAttribute('xlink:href', baseHref + '#check');
// Change the button text
- nearestSpan = button.querySelector("span")
+ let nearestSpan = button.querySelector("span")
+ let original_text = nearestSpan.innerText
nearestSpan.innerText = "Copied to clipboard"
setTimeout(function() {
// Change back to the copy icon
buttonIcon.setAttribute('xlink:href', currentHref);
- if (button.classList.contains('usa-button__small-text')) {
- nearestSpan.innerText = "Copy email";
- } else {
- nearestSpan.innerText = "Copy";
- }
+ nearestSpan.innerText = original_text;
}, 2000);
}
-
}).catch(function(error) {
console.error('Clipboard copy failed', error);
});
@@ -255,7 +247,7 @@ function addOrRemoveSessionBoolean(name, add){
}
function handleClipboardButtons() {
- clipboardButtons = document.querySelectorAll(".usa-button__clipboard")
+ clipboardButtons = document.querySelectorAll(".copy-to-clipboard")
clipboardButtons.forEach((button) => {
// Handle copying the text to your clipboard,
@@ -278,20 +270,7 @@ function addOrRemoveSessionBoolean(name, add){
});
}
- function handleClipboardLinks() {
- let emailButtons = document.querySelectorAll(".usa-button__clipboard-link");
- if (emailButtons){
- emailButtons.forEach((button) => {
- button.addEventListener("click", ()=>{
- copyInnerTextToClipboard(button);
- })
- });
- }
- }
-
handleClipboardButtons();
- handleClipboardLinks();
-
})();
@@ -605,3 +584,169 @@ function initializeWidgetOnList(list, parentId) {
}
}
})();
+
+
+/** An IIFE for copy summary button (appears in DomainRegistry models)
+*/
+(function (){
+ const copyButton = document.getElementById('id-copy-to-clipboard-summary');
+
+ if (copyButton) {
+ copyButton.addEventListener('click', function() {
+ /// Generate a rich HTML summary text and copy to clipboard
+
+ //------ Organization Type
+ const organizationTypeElement = document.getElementById('id_organization_type');
+ const organizationType = organizationTypeElement.options[organizationTypeElement.selectedIndex].text;
+
+ //------ Alternative Domains
+ const alternativeDomainsDiv = document.querySelector('.form-row.field-alternative_domains .readonly');
+ const alternativeDomainslinks = alternativeDomainsDiv.querySelectorAll('a');
+ const alternativeDomains = Array.from(alternativeDomainslinks).map(link => link.textContent);
+
+ //------ Existing Websites
+ const existingWebsitesDiv = document.querySelector('.form-row.field-current_websites .readonly');
+ const existingWebsiteslinks = existingWebsitesDiv.querySelectorAll('a');
+ const existingWebsites = Array.from(existingWebsiteslinks).map(link => link.textContent);
+
+ //------ Additional Contacts
+ // 1 - Create a hyperlinks map so we can display contact details and also link to the contact
+ const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly');
+ let otherContactLinks = [];
+ const nameToUrlMap = {};
+ if (otherContactsDiv) {
+ otherContactLinks = otherContactsDiv.querySelectorAll('a');
+ otherContactLinks.forEach(link => {
+ const name = link.textContent.trim();
+ const url = link.href;
+ nameToUrlMap[name] = url;
+ });
+ }
+
+ // 2 - Iterate through contact details and assemble html for summary
+ let otherContactsSummary = ""
+ const bulletList = document.createElement('ul');
+
+ // CASE 1 - Contacts are not in a table (this happens if there is only one or two other contacts)
+ const contacts = document.querySelectorAll('.field-other_contacts .dja-detail-list dd');
+ if (contacts) {
+ contacts.forEach(contact => {
+ // Check if the
element is not empty
+ const name = contact.querySelector('a#contact_info_name')?.innerText;
+ const title = contact.querySelector('span#contact_info_title')?.innerText;
+ const email = contact.querySelector('span#contact_info_email')?.innerText;
+ const phone = contact.querySelector('span#contact_info_phone')?.innerText;
+ const url = nameToUrlMap[name] || '#';
+ // Format the contact information
+ const listItem = document.createElement('li');
+ listItem.innerHTML = `${name} , ${title}, ${email}, ${phone}`;
+ bulletList.appendChild(listItem);
+ });
+
+ }
+
+ // CASE 2 - Contacts are in a table (this happens if there is more than 2 contacts)
+ const otherContactsTable = document.querySelector('.form-row.field-other_contacts table tbody');
+ if (otherContactsTable) {
+ const otherContactsRows = otherContactsTable.querySelectorAll('tr');
+ otherContactsRows.forEach(contactRow => {
+ // Extract the contact details
+ const name = contactRow.querySelector('th').textContent.trim();
+ const title = contactRow.querySelectorAll('td')[0].textContent.trim();
+ const email = contactRow.querySelectorAll('td')[1].textContent.trim();
+ const phone = contactRow.querySelectorAll('td')[2].textContent.trim();
+ const url = nameToUrlMap[name] || '#';
+ // Format the contact information
+ const listItem = document.createElement('li');
+ listItem.innerHTML = `${name} , ${title}, ${email}, ${phone}`;
+ bulletList.appendChild(listItem);
+ });
+ }
+ otherContactsSummary += bulletList.outerHTML
+
+
+ //------ Requested Domains
+ const requestedDomainElement = document.getElementById('id_requested_domain');
+ const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text;
+
+ //------ Submitter
+ // Function to extract text by ID and handle missing elements
+ function extractTextById(id, divElement) {
+ if (divElement) {
+ const element = divElement.querySelector(`#${id}`);
+ return element ? ", " + element.textContent.trim() : '';
+ }
+ return '';
+ }
+ // Extract the submitter name, title, email, and phone number
+ const submitterDiv = document.querySelector('.form-row.field-submitter');
+ const submitterNameElement = document.getElementById('id_submitter');
+ const submitterName = submitterNameElement.options[submitterNameElement.selectedIndex].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
+ const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official');
+ const seniorOfficialElement = document.getElementById('id_senior_official');
+ const seniorOfficialName = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text;
+ const seniorOfficialTitle = extractTextById('contact_info_title', seniorOfficialDiv);
+ const seniorOfficialEmail = extractTextById('contact_info_email', seniorOfficialDiv);
+ const seniorOfficialPhone = extractTextById('contact_info_phone', seniorOfficialDiv);
+ let seniorOfficialInfo = `${seniorOfficialName}${seniorOfficialTitle}${seniorOfficialEmail}${seniorOfficialPhone}`;
+
+ const html_summary = `Recommendation: ` +
+ `Organization Type: ${organizationType}` +
+ `Requested Domain: ${requestedDomain}` +
+ `Current Websites: ${existingWebsites.join(', ')}` +
+ `Rationale: ` +
+ `Alternative Domains: ${alternativeDomains.join(', ')}` +
+ `Submitter: ${submitterInfo}` +
+ `Senior Official: ${seniorOfficialInfo}` +
+ `Other Employees: ${otherContactsSummary}`;
+
+ //Replace with \n, then strip out all remaining html tags (replace <...> with '')
+ const plain_summary = html_summary.replace(/<\/br>| /g, '\n').replace(/<\/?[^>]+(>|$)/g, '');
+
+ // Create Blobs with the summary content
+ const html_blob = new Blob([html_summary], { type: 'text/html' });
+ const plain_blob = new Blob([plain_summary], { type: 'text/plain' });
+
+ // Create a ClipboardItem with the Blobs
+ const clipboardItem = new ClipboardItem({
+ 'text/html': html_blob,
+ 'text/plain': plain_blob
+ });
+
+ // Write the ClipboardItem to the clipboard
+ navigator.clipboard.write([clipboardItem]).then(() => {
+ // Change the icon to a checkmark on successful copy
+ let buttonIcon = copyButton.querySelector('use');
+ if (buttonIcon) {
+ let currentHref = buttonIcon.getAttribute('xlink:href');
+ let baseHref = currentHref.split('#')[0];
+
+ // Append the new icon reference
+ buttonIcon.setAttribute('xlink:href', baseHref + '#check');
+
+ // Change the button text
+ nearestSpan = copyButton.querySelector("span")
+ original_text = nearestSpan.innerText
+ nearestSpan.innerText = "Copied to clipboard"
+
+ setTimeout(function() {
+ // Change back to the copy icon
+ buttonIcon.setAttribute('xlink:href', currentHref);
+ nearestSpan.innerText = original_text
+ }, 2000);
+
+ }
+ console.log('Summary copied to clipboard successfully!');
+ }).catch(err => {
+ console.error('Failed to copy text: ', err);
+ });
+ });
+ }
+})();
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index d7d116046..711bfe960 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -369,9 +369,6 @@ input.admin-confirm-button {
padding: 10px 8px;
line-height: normal;
}
- .usa-icon {
- top: 2px;
- }
a.button:active, a.button:focus {
text-decoration: none;
}
@@ -447,15 +444,12 @@ address.margin-top-neg-1__detail-list {
}
}
-td button.usa-button__clipboard-link, address.dja-address-contact-list {
+address.dja-address-contact-list {
font-size: unset;
}
address.dja-address-contact-list {
color: var(--body-quiet-color);
- button.usa-button__clipboard-link {
- font-size: unset;
- }
}
// Mimic the normal label size
@@ -464,11 +458,18 @@ address.dja-address-contact-list {
font-size: 0.875rem;
color: var(--body-quiet-color);
}
+}
- address button.usa-button__clipboard-link, td button.usa-button__clipboard-link {
- font-size: 0.875rem !important;
- }
+// Targets the unstyled buttons in the form
+.button--clipboard {
+ color: var(--link-fg);
+}
+// Targets the DJA buttom with a nested icon
+button .usa-icon,
+.button .usa-icon,
+.button--clipboard .usa-icon {
+ vertical-align: middle;
}
.errors span.select2-selection {
@@ -663,7 +664,7 @@ address.dja-address-contact-list {
align-items: center;
- .usa-button__icon {
+ .usa-button--icon {
position: absolute;
right: auto;
left: 4px;
@@ -681,10 +682,6 @@ address.dja-address-contact-list {
}
}
-button.usa-button__clipboard {
- color: var(--link-fg);
-}
-
.no-outline-on-click:focus {
outline: none !important;
}
diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss
index 7fa379c0b..d246366d8 100644
--- a/src/registrar/assets/sass/_theme/_buttons.scss
+++ b/src/registrar/assets/sass/_theme/_buttons.scss
@@ -213,4 +213,4 @@ a.usa-button--unstyled:visited {
.margin-right-neg-4px {
margin-right: -4px;
-}
+}
\ No newline at end of file
diff --git a/src/registrar/assets/sass/_theme/_links.scss b/src/registrar/assets/sass/_theme/_links.scss
index e9b71733a..fd1c3dee9 100644
--- a/src/registrar/assets/sass/_theme/_links.scss
+++ b/src/registrar/assets/sass/_theme/_links.scss
@@ -15,3 +15,4 @@
margin-right: units(0.5);
}
}
+
diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss
index 4775b60c9..f9df015b4 100644
--- a/src/registrar/assets/sass/_theme/styles.scss
+++ b/src/registrar/assets/sass/_theme/styles.scss
@@ -27,4 +27,4 @@
/*--------------------------------------------------
--- Admin ---------------------------------*/
-@forward "admin";
+@forward "admin";
\ No newline at end of file
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index 2f20aeb66..9d707a533 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -664,6 +664,7 @@ ALLOWED_HOSTS = [
"getgov-stable.app.cloud.gov",
"getgov-staging.app.cloud.gov",
"getgov-development.app.cloud.gov",
+ "getgov-ad.app.cloud.gov",
"getgov-ms.app.cloud.gov",
"getgov-ag.app.cloud.gov",
"getgov-litterbox.app.cloud.gov",
diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py
index 74fd4d15d..7ce63d364 100644
--- a/src/registrar/fixtures_users.py
+++ b/src/registrar/fixtures_users.py
@@ -22,6 +22,11 @@ class UserFixture:
"""
ADMINS = [
+ {
+ "username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf",
+ "first_name": "Aditi",
+ "last_name": "Green",
+ },
{
"username": "be17c826-e200-4999-9389-2ded48c43691",
"first_name": "Matthew",
@@ -120,6 +125,11 @@ class UserFixture:
]
STAFF = [
+ {
+ "username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54",
+ "first_name": "Aditi-Analyst",
+ "last_name": "Green-Analyst",
+ },
{
"username": "d6bf296b-fac5-47ff-9c12-f88ccc5c1b99",
"first_name": "Matthew-Analyst",
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index a7252e16b..363de213b 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -215,6 +215,11 @@ class DomainRequest(TimeStampedModel):
}
return org_election_map
+ @classmethod
+ def get_org_label(cls, org_name: str):
+ # Translating the key that is given to the direct readable value
+ return cls(org_name).label if org_name else None
+
class OrganizationChoicesVerbose(models.TextChoices):
"""
Tertiary organization choices
diff --git a/src/registrar/templates/admin/change_form.html b/src/registrar/templates/admin/change_form.html
index 78dac9ac0..f2ac7f2df 100644
--- a/src/registrar/templates/admin/change_form.html
+++ b/src/registrar/templates/admin/change_form.html
@@ -1,4 +1,5 @@
{% extends "admin/change_form.html" %}
+{% load static i18n %}
{% comment %} Replace the Django ul markup with a div. We'll edit the child markup accordingly in change_form_object_tools {% endcomment %}
{% block object-tools %}
@@ -9,4 +10,4 @@
{% endblock %}
{% endif %}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html
index 28c655bbc..198140c19 100644
--- a/src/registrar/templates/admin/change_form_object_tools.html
+++ b/src/registrar/templates/admin/change_form_object_tools.html
@@ -1,4 +1,5 @@
{% load i18n admin_urls %}
+{% load i18n static %}
{% comment %} Replace li with p for more semantic HTML if we have a single child {% endcomment %}
{% block object-tools-items %}
@@ -13,8 +14,21 @@
{% else %}
-
- {% translate "History" %}
-
+
{% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
+
diff --git a/src/registrar/templates/admin/input_with_clipboard.html b/src/registrar/templates/admin/input_with_clipboard.html
index 20a029bed..ea2fbce33 100644
--- a/src/registrar/templates/admin/input_with_clipboard.html
+++ b/src/registrar/templates/admin/input_with_clipboard.html
@@ -8,7 +8,7 @@ Template for an input field with a clipboard
{{ field }}
@@ -25,7 +25,7 @@ Template for an input field with a clipboard