diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md
index d63cf2f94..a0825ab52 100644
--- a/.github/ISSUE_TEMPLATE/developer-onboarding.md
+++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md
@@ -14,13 +14,29 @@ assignees: abroddrick
## Installation
-There are several tools we use locally that you will need to have.
-- [ ] [Install the cf CLI v7](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) for the ability to deploy
+There are several tools we use locally that you will need to have.
+
+- [ ] [Cloudfoundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) Note: If you are on Windows the cli will be under `cf8` or `cf7` depending on which version you install.
- If you are using Windows, installation information can be found [here](https://github.com/cloudfoundry/cli/wiki/V8-CLI-Installation-Guide#installers-and-compressed-binaries)
- Alternatively, for Windows, [consider using chocolately](https://community.chocolatey.org/packages/cloudfoundry-cli/7.2.0)
-- [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg)
- - Alternatively, you can skip this step and [use ssh keys](#setting-up-commit-signing-with-ssh) instead
-- [ ] Install the [Github CLI](https://cli.github.com/)
+- [ ] [GPG](https://gnupg.org/download/)
+ - Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg)
+ - This may not work on DHS devices. Alternatively, you can [use ssh keys](#setting-up-commit-signing-with-ssh) instead.
+- [ ] Docker Community Edition*
+- [ ] Git*
+- [ ] VSCode (our preferred editor)*
+- [ ] Github Desktop* or the Github CLI*
+
+The following tools are optional but recommended. For DHS devices, these can be requested through the DHS IT portal:
+- [ ] Slack Desktop App**
+- [ ] Python 3.10*
+- [ ] NodeJS (latest version available)*
+- [ ] Putty*
+- [ ] Windows Subsystem for Linux*
+
+* Must be requested through DHS IT portal on DHS devices
+
+** Downloadable via DHS Software Center
## Access
@@ -37,7 +53,12 @@ cf login -a api.fr.cloud.gov --sso
**Note:** As mentioned in the [Login documentation](https://developers.login.gov/testing/), the sandbox Login account is different account from your regular, production Login account. If you have not created a Login account for the sandbox before, you will need to create a new account first.
-- [ ] Optional- add yourself as a codeowner if desired. See the [Developer readme](https://github.com/cisagov/getgov/blob/main/docs/developer/README.md) for how to do this and what it does.
+Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Confirm you successfully set up the following accounts:
+- [ ] Identity sandbox accounts - 1 superuser access account and 1 analyst access account.
+- [ ] Login.gov account to access stable
+
+**Optional**
+- [ ] Add yourself as a codeowner if desired. See the [Developer readme](https://github.com/cisagov/getgov/blob/main/docs/developer/README.md) for how to do this and what it does.
### Steps for the onboarder
- [ ] Add the onboardee to cloud.gov org (cisa-dotgov)
@@ -124,3 +145,19 @@ Additionally, consider a gpg key manager like Kleopatra if you run into issues w
We have three types of environments: stable, staging, and sandbox. Stable (production)and staging (pre-prod) get deployed via tagged release, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable or staging. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.
All automation for setting up a developer sandbox is documented in the scripts for [creating a developer sandbox](../../ops/scripts/create_dev_sandbox.sh) and [removing a developer sandbox](../../ops/scripts/destroy_dev_sandbox.sh). A Cloud.gov organization administrator will have to perform the script in order to create the sandbox.
+
+## Known Issues
+
+### SSL Verification Failure
+Some developers using Government Furnished Equipment (GFE) have problems using tools such as git and pip due to SSL verification failurse. This happens because GFE has a custom certificate chain installed, but these tools use their own certificate bundles. As a result, when they try to verify an ssl connection, they cannot and so the connection fails. To resolve this in pip you can use --use-feature=truststore to direct pip to use the local certificate store. If you are running into this issue when using git on windows, run ```git config --global http.sslbackend schannel```.
+
+If you are running into these issues in a docker container you will need to export the root certificate and pull it into the container. Ask another developer how to do this properly.
+
+### Puppeteer Download Error
+When building the node image either individually or with docker compose, there may be an error caused by a node package call puppeteer. This can be resolved by adding `ENV PUPPETEER_SKIP_DOWNLOAD=true` to [node.Dockerfile](../../src/node.Dockerfile) after the COPY command.
+
+### Checksum Error
+There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. In the meantime we have a [workaround](#developing-using-docker).
+
+## Developing Using Docker
+While we have unresolved issues with certain devices, you can pull a pre-built docker image from matthewswspence/getgov-base that comes with all the needed packages installed. To do this, you will need to change the very first line in the main [Dockerfile](../../src/Dockerfile) to `FROM matthewswspence/getgov-base:latest`. Note: this change will need to be reverted before any branch can be merged. Additionally, this will only resolve the [checksum error](#checksum-error), you will still need to resolve any other issues through the listed instructions. We are actively working to resolve this inconvenience.
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/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index a60a59673..0712da0f7 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -1169,6 +1169,8 @@ document.addEventListener('DOMContentLoaded', function() {
const statusIndicator = document.querySelector('.domain__filter-indicator');
const statusToggle = document.querySelector('.usa-button--filter');
const noPortfolioFlag = document.getElementById('no-portfolio-js-flag');
+ const portfolioElement = document.getElementById('portfolio-js-value');
+ const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
/**
* Loads rows in the domains list, as well as updates pagination around the domains list
@@ -1178,10 +1180,15 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} order - the sort order {asc, desc}
* @param {*} scroll - control for the scrollToElement functionality
* @param {*} searchTerm - the search term
+ * @param {*} portfolio - the portfolio id
*/
- function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm) {
+ function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
// fetch json of page of domains, given params
- fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`)
+ let url = `/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`
+ if (portfolio)
+ url += `&portfolio=${portfolio}`
+
+ fetch(url)
.then(response => response.json())
.then(data => {
if (data.error) {
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 3da0a104a..9d707a533 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -241,7 +241,6 @@ TEMPLATES = [
"registrar.context_processors.is_demo_site",
"registrar.context_processors.is_production",
"registrar.context_processors.org_user_status",
- "registrar.context_processors.add_portfolio_to_context",
"registrar.context_processors.add_path_to_context",
"registrar.context_processors.add_has_profile_feature_flag_to_context",
"registrar.context_processors.portfolio_permissions",
@@ -665,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/config/urls.py b/src/registrar/config/urls.py
index 7f9db0e41..90137c4af 100644
--- a/src/registrar/config/urls.py
+++ b/src/registrar/config/urls.py
@@ -26,7 +26,6 @@ from registrar.views.domain_request import Step
from registrar.views.domain_requests_json import get_domain_requests_json
from registrar.views.domains_json import get_domains_json
from registrar.views.utility import always_404
-from registrar.views.portfolios import PortfolioDomainsView, PortfolioDomainRequestsView, PortfolioOrganizationView
from api.views import available, get_current_federal, get_current_full
@@ -61,19 +60,19 @@ for step, view in [
urlpatterns = [
path("", views.index, name="home"),
path(
- "portfolio//domains/",
- PortfolioDomainsView.as_view(),
- name="portfolio-domains",
+ "domains/",
+ views.PortfolioDomainsView.as_view(),
+ name="domains",
),
path(
- "portfolio//domain_requests/",
- PortfolioDomainRequestsView.as_view(),
- name="portfolio-domain-requests",
+ "requests/",
+ views.PortfolioDomainRequestsView.as_view(),
+ name="domain-requests",
),
path(
- "portfolio//organization/",
- PortfolioOrganizationView.as_view(),
- name="portfolio-organization",
+ "organization/",
+ views.PortfolioOrganizationView.as_view(),
+ name="organization",
),
path(
"admin/logout/",
diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py
index 06ef07050..861a4e701 100644
--- a/src/registrar/context_processors.py
+++ b/src/registrar/context_processors.py
@@ -50,10 +50,6 @@ def org_user_status(request):
}
-def add_portfolio_to_context(request):
- return {"portfolio": getattr(request, "portfolio", None)}
-
-
def add_path_to_context(request):
return {"path": getattr(request, "path", None)}
@@ -70,11 +66,15 @@ def portfolio_permissions(request):
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
+ "portfolio": None,
+ "has_organization_feature_flag": False,
}
return {
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(),
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(),
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(),
+ "portfolio": request.user.portfolio,
+ "has_organization_feature_flag": flag_is_active(request, "organization_feature"),
}
except AttributeError:
# Handles cases where request.user might not exist
@@ -82,4 +82,6 @@ def portfolio_permissions(request):
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
+ "portfolio": None,
+ "has_organization_feature_flag": False,
}
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/management/commands/clean_tables.py b/src/registrar/management/commands/clean_tables.py
index f0c51390b..5d4439d95 100644
--- a/src/registrar/management/commands/clean_tables.py
+++ b/src/registrar/management/commands/clean_tables.py
@@ -56,14 +56,27 @@ class Command(BaseCommand):
self.clean_table(table_name)
def clean_table(self, table_name):
- """Delete all rows in the given table"""
+ """Delete all rows in the given table.
+
+ Delete in batches to be able to handle large tables"""
try:
# Get the model class dynamically
model = apps.get_model("registrar", table_name)
- # Use a transaction to ensure database integrity
- with transaction.atomic():
- model.objects.all().delete()
- logger.info(f"Successfully cleaned table {table_name}")
+ BATCH_SIZE = 1000
+ total_deleted = 0
+
+ # Get initial batch of primary keys
+ pks = list(model.objects.values_list("pk", flat=True)[:BATCH_SIZE])
+
+ while pks:
+ # Use a transaction to ensure database integrity
+ with transaction.atomic():
+ deleted, _ = model.objects.filter(pk__in=pks).delete()
+ total_deleted += deleted
+ logger.debug(f"Deleted {deleted} {table_name}s, total deleted: {total_deleted}")
+ # Get the next batch of primary keys
+ pks = list(model.objects.values_list("pk", flat=True)[:BATCH_SIZE])
+ logger.info(f"Successfully cleaned table {table_name}, deleted {total_deleted} rows")
except LookupError:
logger.error(f"Model for table {table_name} not found.")
except Exception as e:
diff --git a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py
new file mode 100644
index 000000000..55645298f
--- /dev/null
+++ b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.2.10 on 2024-07-25 12:45
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("registrar", "0113_user_portfolio_user_portfolio_additional_permissions_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="user",
+ name="portfolio_additional_permissions",
+ field=django.contrib.postgres.fields.ArrayField(
+ base_field=models.CharField(
+ choices=[
+ ("view_all_domains", "View all domains and domain reports"),
+ ("view_managed_domains", "View managed domains"),
+ ("view_member", "View members"),
+ ("edit_member", "Create and edit members"),
+ ("view_all_requests", "View all requests"),
+ ("view_created_requests", "View created requests"),
+ ("edit_requests", "Create and edit requests"),
+ ("view_portfolio", "View organization"),
+ ("edit_portfolio", "Edit organization"),
+ ],
+ max_length=50,
+ ),
+ blank=True,
+ help_text="Select one or more additional permissions.",
+ null=True,
+ size=None,
+ ),
+ ),
+ ]
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/models/user.py b/src/registrar/models/user.py
index b135e30c7..b1c9473db 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -76,11 +76,6 @@ class User(AbstractUser):
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
- # EDIT_DOMAINS is really self.domains. We add is hear and leverage it in has_permission
- # so we have one way to test for portfolio and domain edit permissions
- # Do we need to check for portfolio domains specifically?
- # NOTE: A user on an org can currently invite a user outside the org
- EDIT_DOMAINS = "edit_domains", "User is a manager on a domain"
VIEW_MEMBER = "view_member", "View members"
EDIT_MEMBER = "edit_member", "Create and edit members"
@@ -268,11 +263,6 @@ class User(AbstractUser):
def _has_portfolio_permission(self, portfolio_permission):
"""The views should only call this function when testing for perms and not rely on roles."""
- # EDIT_DOMAINS === user is a manager on a domain (has UserDomainRole)
- # NOTE: Should we check whether the domain is in the portfolio?
- if portfolio_permission == self.UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists():
- return True
-
if not self.portfolio:
return False
@@ -286,21 +276,14 @@ class User(AbstractUser):
return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
def has_domains_portfolio_permission(self):
- return (
- self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
- or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
- # or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS)
- )
-
- def has_edit_domains_portfolio_permission(self):
- return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS)
+ return self._has_portfolio_permission(
+ User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
+ ) or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
def has_domain_requests_portfolio_permission(self):
- return (
- self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
- or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
- # or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_REQUESTS)
- )
+ return self._has_portfolio_permission(
+ User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
+ ) or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
@classmethod
def needs_identity_verification(cls, email, uuid):
diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py
index dd9b5541a..2af331bc9 100644
--- a/src/registrar/registrar_middleware.py
+++ b/src/registrar/registrar_middleware.py
@@ -149,10 +149,10 @@ class CheckPortfolioMiddleware:
request.portfolio = portfolio
if request.user.has_domains_portfolio_permission():
- portfolio_redirect = reverse("portfolio-domains", kwargs={"portfolio_id": portfolio.id})
+ portfolio_redirect = reverse("domains")
else:
# View organization is the lowest access
- portfolio_redirect = reverse("portfolio-organization", kwargs={"portfolio_id": portfolio.id})
+ portfolio_redirect = reverse("organization")
return HttpResponseRedirect(portfolio_redirect)
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 %}
-
{% 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