From 99fde890175608b272f9304d5bfd258d78f10b83 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Thu, 20 Jun 2024 17:16:40 -0400 Subject: [PATCH 01/70] Update domain_invitation_description.html added more detail to give better direction to analysts --- .../includes/descriptions/domain_invitation_description.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 7765b9203..3a5609ee8 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -5,8 +5,8 @@ accept and become a domain manager.

-An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. -A “received” status indicates that the recipient has logged in. +An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. +A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 9d116df83fd29f7f056504b877c599d7296c2d73 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 21 Jun 2024 21:46:31 -0400 Subject: [PATCH 02/70] Update src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- .../includes/descriptions/domain_invitation_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 3a5609ee8..03d0cd99a 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -6,7 +6,7 @@ accept and become a domain manager.

An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. -A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination. +A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 64933e02c60c1ffffdf7f09d7c12430450076870 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Tue, 25 Jun 2024 09:32:46 -0400 Subject: [PATCH 03/70] Update test_admin.py adding another "invited" --- src/registrar/tests/test_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 802974b6e..798dfaad8 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2820,7 +2820,7 @@ class TestDomainInvitationAdmin(TestCase): ) # Assert that the filters are added - self.assertContains(response, "invited", count=4) + self.assertContains(response, "invited", count=5) self.assertContains(response, "Invited", count=2) self.assertContains(response, "retrieved", count=2) self.assertContains(response, "Retrieved", count=2) From 2e28a0ffe5c22a059bb136bcd64ba16e4f5b0dcb Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Tue, 25 Jun 2024 12:53:08 -0400 Subject: [PATCH 04/70] Update src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html Co-authored-by: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> --- .../includes/descriptions/domain_invitation_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 03d0cd99a..23c617f1c 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -5,7 +5,7 @@ accept and become a domain manager.

-An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. +An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with an "invited" status will prevent the user from signing in. A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From b4214300246ecd0306bff93983370a6ee61b8312 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 17 Jul 2024 10:50:01 -0700 Subject: [PATCH 05/70] Test out copy functionality --- src/registrar/templates/admin/change_form.html | 5 +++++ .../templates/admin/change_form_object_tools.html | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/change_form.html b/src/registrar/templates/admin/change_form.html index 78dac9ac0..3efbe554e 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 %} @@ -10,3 +11,7 @@ {% endif %} {% endblock %} + +{% block extrahead %} + +{% 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..48a6b101d 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -16,5 +16,12 @@

{% translate "History" %}

+ {% if opts.model_name == 'domainrequest' %} +

+ + +

+ {% endif %} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} + From 721e91aac75b77742c229dc7516d03fe865bd3b8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 17 Jul 2024 11:05:09 -0700 Subject: [PATCH 06/70] Add in js file --- src/registrar/assets/js/copy-summary.js | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/registrar/assets/js/copy-summary.js diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js new file mode 100644 index 000000000..42cedf008 --- /dev/null +++ b/src/registrar/assets/js/copy-summary.js @@ -0,0 +1,34 @@ + +document.addEventListener('DOMContentLoaded', function() { + document.getElementById('copy-summary-btn').addEventListener('click', function() { + // Generate the summary text + const organizationType = document.getElementById('id_organization_type').value; + const requestedDomain = document.getElementById('id_requested_domain').value; + const existingWebsites = Array.from(document.querySelectorAll('#id_current_websites')).map(el => el.text).join(', '); + const alternativeDomains = Array.from(document.querySelectorAll('#id_alternative_domains')).map(el => el.text).join(', '); + const submitter = document.getElementById('id_submitter').value; + const seniorOfficial = document.getElementById('id_senior_official').value; + const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n* '); + + const summary = `*Recommendation:*\n\n` + + `*Organization Type:* ${organizationType}\n\n` + + `*Requested Domain:* ${requestedDomain}\n\n` + + `*Existing website(s):*\n${existingWebsites}\n\n` + + `*Rationale:*\n\n` + + `*Alternate Domain(s):*\n* ${alternativeDomains.split(', ').join('\n* ')}\n\n` + + `*Submitter:*\n\n* ${submitter}\n\n` + + `*Senior Official:*\n\n* ${seniorOfficial}\n\n` + + `*Additional Contact(s):*\n\n* ${otherContacts}\n\n`; + + // Create a temporary textarea element to hold the summary + const textArea = document.createElement('textarea'); + textArea.value = summary; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + alert('Summary copied to clipboard!'); + alert("hello"); + }); +}); From e19bcaf55f61cc52d7f43cd38df19378d47290e8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 17 Jul 2024 11:05:42 -0700 Subject: [PATCH 07/70] Remove extra line --- src/registrar/assets/js/copy-summary.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 42cedf008..9edad3f6b 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -29,6 +29,5 @@ document.addEventListener('DOMContentLoaded', function() { document.body.removeChild(textArea); alert('Summary copied to clipboard!'); - alert("hello"); }); }); From 17274a606d7b3a8bb44d34934fd53491e53267e4 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:09:50 -0700 Subject: [PATCH 08/70] Removed unused comment job from manual deploy --- .github/workflows/deploy-sandbox.yaml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 4bd7f99dd..7f8637a04 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -64,20 +64,4 @@ jobs: cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ env.ENVIRONMENT }} - cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml - comment: - runs-on: ubuntu-latest - needs: [variables, deploy] - steps: - - uses: actions/github-script@v6 - env: - ENVIRONMENT: ${{ needs.variables.outputs.environment }} - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.' - }) \ No newline at end of file + cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml \ No newline at end of file From f2fd764d6735c4a8bc177c5466ba6cfea6cf668c Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:20:23 -0700 Subject: [PATCH 09/70] Correct removed comment job in deploy workflow --- ...anch-to-sandbox.yaml => deploy-manual.yaml} | 16 ---------------- .github/workflows/deploy-sandbox.yaml | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 17 deletions(-) rename .github/workflows/{deploy-branch-to-sandbox.yaml => deploy-manual.yaml} (78%) diff --git a/.github/workflows/deploy-branch-to-sandbox.yaml b/.github/workflows/deploy-manual.yaml similarity index 78% rename from .github/workflows/deploy-branch-to-sandbox.yaml rename to .github/workflows/deploy-manual.yaml index 14a3d8ef8..97415a0d9 100644 --- a/.github/workflows/deploy-branch-to-sandbox.yaml +++ b/.github/workflows/deploy-manual.yaml @@ -71,20 +71,4 @@ jobs: cf_org: cisa-dotgov cf_space: ${{ env.ENVIRONMENT }} cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml - comment: - runs-on: ubuntu-latest - needs: [deploy] - steps: - - uses: actions/github-script@v6 - env: - ENVIRONMENT: ${{ github.event.inputs.environment }} - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.' - }) - diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 7f8637a04..4bd7f99dd 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -64,4 +64,20 @@ jobs: cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ env.ENVIRONMENT }} - cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml \ No newline at end of file + cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml + comment: + runs-on: ubuntu-latest + needs: [variables, deploy] + steps: + - uses: actions/github-script@v6 + env: + ENVIRONMENT: ${{ needs.variables.outputs.environment }} + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.' + }) \ No newline at end of file From 7820c362433d714c1e47a9aa0de7ad75dcd1967b Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:21:55 -0700 Subject: [PATCH 10/70] Revert workflow name for testing purposes --- .../{deploy-manual.yaml => deploy-branch-to-sandbox.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{deploy-manual.yaml => deploy-branch-to-sandbox.yaml} (100%) diff --git a/.github/workflows/deploy-manual.yaml b/.github/workflows/deploy-branch-to-sandbox.yaml similarity index 100% rename from .github/workflows/deploy-manual.yaml rename to .github/workflows/deploy-branch-to-sandbox.yaml From 3e5401a549d0fc4b8abb756730482a1d6e23dbc3 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:31:20 -0700 Subject: [PATCH 11/70] Add new sandboxes to deploy-manual workflow. Rename workflow name to deploy-manual --- .../{deploy-branch-to-sandbox.yaml => deploy-manual.yaml} | 0 ops/scripts/create_dev_sandbox.sh | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) rename .github/workflows/{deploy-branch-to-sandbox.yaml => deploy-manual.yaml} (100%) diff --git a/.github/workflows/deploy-branch-to-sandbox.yaml b/.github/workflows/deploy-manual.yaml similarity index 100% rename from .github/workflows/deploy-branch-to-sandbox.yaml rename to .github/workflows/deploy-manual.yaml diff --git a/ops/scripts/create_dev_sandbox.sh b/ops/scripts/create_dev_sandbox.sh index 676fcf7ae..b3a970584 100755 --- a/ops/scripts/create_dev_sandbox.sh +++ b/ops/scripts/create_dev_sandbox.sh @@ -112,10 +112,14 @@ sed -i '' '/ - development/ {a\ - '"$1"' }' .github/workflows/reset-db.yaml -sed -i '' '/ - development/ {a\ +sed -i '' '/ - backup/ {a\ - '"$1"' }' .github/workflows/migrate.yaml +sed -i '' '/ - development/ {a\ + - '"$1"' +}' .github/workflows/deploy-manual.yaml + sed -i '' '/${{startsWith(github.head_ref, / {a\ || startsWith(github.head_ref, '"'$1'"') }' .github/workflows/deploy-sandbox.yaml From bc334bd41fe276eb386753d931a7e284fc44567c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 22 Jul 2024 16:25:14 -0600 Subject: [PATCH 12/70] updated button format and javascript --- src/registrar/assets/js/copy-summary.js | 51 ++++++++++++------- .../admin/change_form_object_tools.html | 19 +++---- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 9edad3f6b..b268956a5 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -8,25 +8,42 @@ document.addEventListener('DOMContentLoaded', function() { const alternativeDomains = Array.from(document.querySelectorAll('#id_alternative_domains')).map(el => el.text).join(', '); const submitter = document.getElementById('id_submitter').value; const seniorOfficial = document.getElementById('id_senior_official').value; - const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n* '); + const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n '); - const summary = `*Recommendation:*\n\n` + - `*Organization Type:* ${organizationType}\n\n` + - `*Requested Domain:* ${requestedDomain}\n\n` + - `*Existing website(s):*\n${existingWebsites}\n\n` + - `*Rationale:*\n\n` + - `*Alternate Domain(s):*\n* ${alternativeDomains.split(', ').join('\n* ')}\n\n` + - `*Submitter:*\n\n* ${submitter}\n\n` + - `*Senior Official:*\n\n* ${seniorOfficial}\n\n` + - `*Additional Contact(s):*\n\n* ${otherContacts}\n\n`; + const summary = `Recommendation:
` + + `Organization Type: ${organizationType}
` + + `Requested Domain: ${requestedDomain}
` + + `Existing website(s): ${existingWebsites}
` + + `Rationale:` + + `Alternate Domain(s): ${alternativeDomains.split(', ').join('\n ')}
` + + `Submitter: ${submitter}
` + + `Senior Official: ${seniorOfficial}
` + + `Additional Contact(s): ${otherContacts}
`; - // Create a temporary textarea element to hold the summary - const textArea = document.createElement('textarea'); - textArea.value = summary; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); + // Create a temporary element + let tempElement = document.createElement('div'); + tempElement.innerHTML = summary; + // Append the element to the body + document.body.appendChild(tempElement); + + // Use the Selection and Range APIs to select the element's content + let range = document.createRange(); + range.selectNodeContents(tempElement); + let selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + // Use the Clipboard API to write the selected HTML content to the clipboard + navigator.clipboard.write([ + new ClipboardItem({ + 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) + }) + ]).then(() => { + console.log('Bold text copied to clipboard successfully!'); + }).catch(err => { + console.error('Failed to copy text: ', err); + }); + document.body.removeChild(tempElement); alert('Summary copied to clipboard!'); }); diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 48a6b101d..c2d22e9e2 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -13,15 +13,16 @@ {% else %} -

- {% translate "History" %} -

- {% if opts.model_name == 'domainrequest' %} -

- - -

- {% endif %} + {% endif %} {% endblock %} From b245dfb2fe801506b3bbc3a0713d5551be8d8330 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 10:55:02 -0600 Subject: [PATCH 13/70] fixed dom extraction --- src/registrar/assets/js/copy-summary.js | 39 ++++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index b268956a5..d12efb872 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -2,23 +2,40 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('copy-summary-btn').addEventListener('click', function() { // Generate the summary text - const organizationType = document.getElementById('id_organization_type').value; - const requestedDomain = document.getElementById('id_requested_domain').value; - const existingWebsites = Array.from(document.querySelectorAll('#id_current_websites')).map(el => el.text).join(', '); - const alternativeDomains = Array.from(document.querySelectorAll('#id_alternative_domains')).map(el => el.text).join(', '); - const submitter = document.getElementById('id_submitter').value; - const seniorOfficial = document.getElementById('id_senior_official').value; - const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n '); + + const organizationTypeElement = document.getElementById('id_organization_type'); + const organizationType = organizationTypeElement.options[organizationTypeElement.selectedIndex].text; + + const alternativeDomainsDiv = document.querySelector('.form-row.field-alternative_domains .readonly'); + const alternativeDomainslinks = alternativeDomainsDiv.querySelectorAll('a'); + const alternativeDomains = Array.from(alternativeDomainslinks).map(link => link.textContent); + + const existingWebsitesDiv = document.querySelector('.form-row.field-current_websites .readonly'); + const existingWebsiteslinks = existingWebsitesDiv.querySelectorAll('a'); + const existingWebsites = Array.from(existingWebsiteslinks).map(link => link.textContent); + + const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly'); + const otherContactslinks = otherContactsDiv.querySelectorAll('a'); + const otherContacts = Array.from(otherContactslinks).map(link => link.textContent); + + const requestedDomainElement = document.getElementById('id_requested_domain'); + const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text; + + const submitterElement = document.getElementById('id_submitter'); + const submitter = submitterElement.options[submitterElement.selectedIndex].text; + + const seniorOfficialElement = document.getElementById('id_senior_official'); + const seniorOfficial = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; const summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + `Requested Domain: ${requestedDomain}
` + - `Existing website(s): ${existingWebsites}
` + - `Rationale:` + - `Alternate Domain(s): ${alternativeDomains.split(', ').join('\n ')}
` + + `Existing website(s): ${existingWebsites.join('
')}
` + + `Rationale:
` + + `Alternate Domain(s): ${alternativeDomains.join('
')}
` + `Submitter: ${submitter}
` + `Senior Official: ${seniorOfficial}
` + - `Additional Contact(s): ${otherContacts}
`; + `Additional Contact(s): ${otherContacts.join('
')}
`; // Create a temporary element let tempElement = document.createElement('div'); From c33a4b61f579f6e93f238521e0d39a66965e0996 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 13:42:02 -0600 Subject: [PATCH 14/70] fixed formatting --- src/registrar/assets/js/copy-summary.js | 63 ++++++++++++++++--- .../admin/includes/contact_detail_list.html | 19 +++--- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index d12efb872..c10adbffa 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -1,41 +1,86 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('copy-summary-btn').addEventListener('click', function() { - // Generate the summary text + /// Generate the summary text + //------ 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'); - const otherContactslinks = otherContactsDiv.querySelectorAll('a'); - const otherContacts = Array.from(otherContactslinks).map(link => link.textContent); + const otherContactLinks = otherContactsDiv.querySelectorAll('a'); + const nameToUrlMap = {}; + 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 = "" + // Get the table rows of contact details + const otherContactsTable = document.querySelector('.form-row.field-other_contacts table tbody'); + const otherContactsRows = otherContactsTable.querySelectorAll('tr'); + const bulletList = document.createElement('ul'); + 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.innerH = `${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; - const submitterElement = document.getElementById('id_submitter'); - const submitter = submitterElement.options[submitterElement.selectedIndex].text; + //------ Submitter + // Function to extract text by ID and handle missing elements + function extractTextById(id) { + const element = document.getElementById(id); + return element ? element.textContent.trim()+"," : ''; + } + // Extract the submitter name, title, email, and phone number + const submitterName = extractTextById('contact_info_name'); + const submitterTitle = extractTextById('contact_info_title'); + const submitterEmail = extractTextById('contact_info_email'); + const submitterPhone = extractTextById('contact_info_phone'); + // Format the contact information + let submitterInfo = `${submitterName} ${submitterTitle} ${submitterEmail} ${submitterPhone}`; + + //------ Senior Official const seniorOfficialElement = document.getElementById('id_senior_official'); const seniorOfficial = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; const summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + `Requested Domain: ${requestedDomain}
` + - `Existing website(s): ${existingWebsites.join('
')}
` + + `Existing website(s): ${existingWebsites.join(',')}
` + `Rationale:
` + - `Alternate Domain(s): ${alternativeDomains.join('
')}
` + - `Submitter: ${submitter}
` + + `Alternate Domain(s): ${alternativeDomains.join(',')}
` + + `Submitter: ${submitterInfo}
` + `Senior Official: ${seniorOfficial}
` + - `Additional Contact(s): ${otherContacts.join('
')}
`; + `Additional Contact(s): ${otherContactsSummary}
`; // Create a temporary element let tempElement = document.createElement('div'); diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 2ee490d76..8358e5440 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -2,25 +2,28 @@
+ {% if show_formatted_name %} {% if user.get_formatted_name %} - {{ user.get_formatted_name }}
+ {{ user.get_formatted_name }} {% else %} - None
+ None {% endif %} {% endif %} +
{% if user.has_contact_info %} {# Title #} {% if user.title %} - {{ user.title }} -
+ {{ user.title }} {% else %} - None
+ None {% endif %} +
+ {# Email #} {% if user.email %} - {{ user.email }} + {{ user.email }} {% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
{% else %} @@ -29,7 +32,7 @@ {# Phone #} {% if user.phone %} - {{ user.phone }} + {{ user.phone.as_national }}
{% else %} None
@@ -40,6 +43,6 @@ {% endif %} {% if user_verification_type %} - {{ user_verification_type }} + {{ user_verification_type }} {% endif %}
From 4666da4df709502ff283c8b0592426a4088a4eae Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 13:50:40 -0600 Subject: [PATCH 15/70] fix wierd typo --- src/registrar/assets/js/copy-summary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index c10adbffa..cd437f0a6 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -43,7 +43,7 @@ document.addEventListener('DOMContentLoaded', function() { const url = nameToUrlMap[name] || '#'; // Format the contact information const listItem = document.createElement('li'); - listItem.innerH = `${name}, ${title}, ${email}, ${phone}`; + listItem.innerHTML = `${name}, ${title}, ${email}, ${phone}`; bulletList.appendChild(listItem); }); otherContactsSummary += bulletList.outerHTML From 72fdcfdac05e834adac59e59f83ad2d1804134ef Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 14:22:10 -0600 Subject: [PATCH 16/70] more formatting fixes --- src/registrar/assets/js/copy-summary.js | 33 ++++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index cd437f0a6..824002f76 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -55,31 +55,40 @@ document.addEventListener('DOMContentLoaded', function() { //------ Submitter // Function to extract text by ID and handle missing elements - function extractTextById(id) { - const element = document.getElementById(id); - return element ? element.textContent.trim()+"," : ''; + 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 submitterName = extractTextById('contact_info_name'); - const submitterTitle = extractTextById('contact_info_title'); - const submitterEmail = extractTextById('contact_info_email'); - const submitterPhone = extractTextById('contact_info_phone'); - // Format the contact information + 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 seniorOfficial = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; + 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 summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + `Requested Domain: ${requestedDomain}
` + - `Existing website(s): ${existingWebsites.join(',')}
` + + `Existing website(s): ${existingWebsites.join(', ')}
` + `Rationale:
` + - `Alternate Domain(s): ${alternativeDomains.join(',')}
` + + `Alternate Domain(s): ${alternativeDomains.join(', ')}
` + `Submitter: ${submitterInfo}
` + - `Senior Official: ${seniorOfficial}
` + + `Senior Official: ${seniorOfficialInfo}
` + `Additional Contact(s): ${otherContactsSummary}
`; // Create a temporary element From 711c71c1147cfbc5a01d6e3c1c8240bfabd9ae98 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 14:37:58 -0600 Subject: [PATCH 17/70] Catching edge-cases --- src/registrar/assets/js/copy-summary.js | 55 ++++++++++--------- .../admin/includes/contact_detail_list.html | 2 +- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 824002f76..de15596d2 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -1,7 +1,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('copy-summary-btn').addEventListener('click', function() { - /// Generate the summary text + /// Generate a rich HTML summary text and copy to clipboard //------ Organization Type const organizationTypeElement = document.getElementById('id_organization_type'); @@ -20,33 +20,38 @@ document.addEventListener('DOMContentLoaded', function() { //------ 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'); - const otherContactLinks = otherContactsDiv.querySelectorAll('a'); - const nameToUrlMap = {}; - otherContactLinks.forEach(link => { - const name = link.textContent.trim(); - const url = link.href; - nameToUrlMap[name] = url; - }); + let otherContactLinks = []; + if (otherContactsDiv) { + otherContactLinks = otherContactsDiv.querySelectorAll('a'); + const nameToUrlMap = {}; + 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 = "" // Get the table rows of contact details const otherContactsTable = document.querySelector('.form-row.field-other_contacts table tbody'); - const otherContactsRows = otherContactsTable.querySelectorAll('tr'); - const bulletList = document.createElement('ul'); - 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 + if (otherContactsTable) { + const otherContactsRows = otherContactsTable.querySelectorAll('tr'); + const bulletList = document.createElement('ul'); + 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 @@ -69,7 +74,7 @@ document.addEventListener('DOMContentLoaded', function() { 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}`; + let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`; //------ Senior Official @@ -79,7 +84,7 @@ document.addEventListener('DOMContentLoaded', function() { 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}`; + let seniorOfficialInfo = `${seniorOfficialName}${seniorOfficialTitle}${seniorOfficialEmail}${seniorOfficialPhone}`; const summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 8358e5440..418d1464b 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -32,7 +32,7 @@ {# Phone #} {% if user.phone %} - {{ user.phone.as_national }} + {{ user.phone }}
{% else %} None
From ee18be87c7ed8e64277046713cf88447e7003f55 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jul 2024 11:39:48 -0400 Subject: [PATCH 18/70] use css for read only styles --- src/registrar/assets/sass/_theme/_forms.scss | 10 +++ ...rtfolio_additional_permissions_and_more.py | 52 ++++++++++++ src/registrar/models/user.py | 17 ++-- .../includes/finish_profile_form.html | 8 +- .../templates/includes/input_read_only.html | 7 ++ .../templates/includes/input_with_errors.html | 8 +- ...donly_input.html => toggleable_input.html} | 0 ...edit_button.html => toggleable_label.html} | 0 .../templates/portfolio_organization.html | 78 ++++++++++-------- src/registrar/templatetags/field_helpers.py | 4 +- src/registrar/tests/test_views_portfolio.py | 80 ++++++++++--------- src/registrar/views/portfolios.py | 3 + 12 files changed, 179 insertions(+), 88 deletions(-) create mode 100644 src/registrar/migrations/0114_alter_user_portfolio_additional_permissions_and_more.py create mode 100644 src/registrar/templates/includes/input_read_only.html rename src/registrar/templates/includes/{readonly_input.html => toggleable_input.html} (100%) rename src/registrar/templates/includes/{label_with_edit_button.html => toggleable_label.html} (100%) diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index c025bdb29..0aedfcdba 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -82,3 +82,13 @@ legend.float-left-tablet + button.float-right-tablet { color: var(--close-button-hover-bg); } } + +.read-only-label { + font-size: size('body', 'sm'); + color: color('primary'); + margin-bottom: units(0.5); +} + +.read-only-value { + margin-top: units(0); +} diff --git a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions_and_more.py b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions_and_more.py new file mode 100644 index 000000000..f70c5388c --- /dev/null +++ b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.10 on 2024-07-30 02:51 + +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"), + ("edit_domains", "User is a manager on a domain"), + ("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"), + ("edit_portfolio", "Edit organization"), + ], + max_length=50, + ), + blank=True, + help_text="Select one or more additional permissions.", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="user", + name="portfolio_roles", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[("organization_admin", "Admin"), ("organization_admin_read_only", "Admin read only")], + max_length=50, + ), + blank=True, + help_text="Select one or more roles.", + null=True, + size=None, + ), + ), + ] diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index b135e30c7..dd826bc11 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -69,7 +69,7 @@ class User(AbstractUser): ORGANIZATION_ADMIN = "organization_admin", "Admin" ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only" - ORGANIZATION_MEMBER = "organization_member", "Member" + # ORGANIZATION_MEMBER is an abstract role where user.portfolio is true class UserPortfolioPermissionChoices(models.TextChoices): """ """ @@ -89,7 +89,7 @@ class User(AbstractUser): VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests" EDIT_REQUESTS = "edit_requests", "Create and edit requests" - VIEW_PORTFOLIO = "view_portfolio", "View organization" + # VIEW_PORTFOLIO is an abstract permission that returns true when user.portfolio is true EDIT_PORTFOLIO = "edit_portfolio", "Edit organization" PORTFOLIO_ROLE_PERMISSIONS = { @@ -99,17 +99,12 @@ class User(AbstractUser): UserPortfolioPermissionChoices.EDIT_MEMBER, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS, - UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ], UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [ UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, UserPortfolioPermissionChoices.VIEW_MEMBER, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, - UserPortfolioPermissionChoices.VIEW_PORTFOLIO, - ], - UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ - UserPortfolioPermissionChoices.VIEW_PORTFOLIO, ], } @@ -280,10 +275,14 @@ class User(AbstractUser): return portfolio_permission in portfolio_permissions - # the methods below are checks for individual portfolio permissions. they are defined here + # the methods below are checks for individual portfolio permissions. They are defined here # to make them easier to call elsewhere throughout the application def has_base_portfolio_permission(self): - return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO) + """Base role/permission, the user is simply linked to a portfolio""" + return self.portfolio is not None + + def has_edit_org_portfolio_permission(self): + return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO) def has_domains_portfolio_permission(self): return ( diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 88f7a73af..8369511a5 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -35,7 +35,7 @@ - {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %} + {% with toggleable_input=True toggleable_label=True group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} @@ -54,7 +54,7 @@ {% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %} - {% with show_readonly=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %} + {% with toggleable_input=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %} {% with link_href=login_help_url %} {% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %} {% with link_text="Get help with your Login.gov account" target_blank=True do_not_show_max_chars=True %} @@ -64,11 +64,11 @@ {% endwith %} {% endwith %} - {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} + {% with toggleable_input=True toggleable_label=True group_classes="usa-form-editable padding-top-2" %} {% input_with_errors form.title %} {% endwith %} - {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} + {% with toggleable_input=True toggleable_label=True group_classes="usa-form-editable padding-top-2" %} {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/templates/includes/input_read_only.html b/src/registrar/templates/includes/input_read_only.html new file mode 100644 index 000000000..b76f82e8b --- /dev/null +++ b/src/registrar/templates/includes/input_read_only.html @@ -0,0 +1,7 @@ +{% comment %} +Template include for read-only form fields +{% endcomment %} + + +

{{ field.label }}

+

{{ field.value }}

diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 623ec0a33..d1e53968e 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -27,8 +27,8 @@ error messages, if necessary. {% endif %} {% if not field.widget_type == "checkbox" %} - {% if show_edit_button %} - {% include "includes/label_with_edit_button.html" with bold_label=True %} + {% if toggleable_label %} + {% include "includes/toggleable_label.html" with bold_label=True %} {% else %} {% include "django/forms/label.html" %} {% endif %} @@ -63,8 +63,8 @@ error messages, if necessary.
{% endif %} - {% if show_readonly %} - {% include "includes/readonly_input.html" %} + {% if toggleable_input %} + {% include "includes/toggleable_input.html" %} {% endif %} {# this is the input field, itself #} diff --git a/src/registrar/templates/includes/readonly_input.html b/src/registrar/templates/includes/toggleable_input.html similarity index 100% rename from src/registrar/templates/includes/readonly_input.html rename to src/registrar/templates/includes/toggleable_input.html diff --git a/src/registrar/templates/includes/label_with_edit_button.html b/src/registrar/templates/includes/toggleable_label.html similarity index 100% rename from src/registrar/templates/includes/label_with_edit_button.html rename to src/registrar/templates/includes/toggleable_label.html diff --git a/src/registrar/templates/portfolio_organization.html b/src/registrar/templates/portfolio_organization.html index 0dede3c32..cc9cf5b6a 100644 --- a/src/registrar/templates/portfolio_organization.html +++ b/src/registrar/templates/portfolio_organization.html @@ -23,42 +23,56 @@

The name of your federal agency will be publicly listed as the domain registrant.

-

- The federal agency for your organization can’t be updated here. - To suggest an update, email help@get.gov. -

- - {% include "includes/form_errors.html" with form=form %} - - {% include "includes/required_fields.html" %} - -
- {% csrf_token %} - + {% if has_edit_org_portfolio_permission %}

- Federal agency - {{ portfolio }} + The federal agency for your organization can’t be updated here. + To suggest an update, email help@get.gov.

- {% input_with_errors form.address_line1 %} + {% include "includes/form_errors.html" with form=form %} + {% include "includes/required_fields.html" %} + + {% csrf_token %} +

Federal agency

+

+ {{ portfolio }} +

+ {% input_with_errors form.address_line1 %} + {% input_with_errors form.address_line2 %} + {% input_with_errors form.city %} + {% input_with_errors form.state_territory %} + {% with add_class="usa-input--small" %} + {% input_with_errors form.zipcode %} + {% endwith %} + +
+ {% else %} +

Federal agency

+

+ {{ portfolio }} +

+ {% if form.address_line1.value is not None %} + {% include "includes/input_read_only.html" with field=form.address_line1 %} + {% endif %} + {% if form.address_line2.value is not None %} + {% include "includes/input_read_only.html" with field=form.address_line2 %} + {% endif %} + {% if form.city.value is not None %} + {% include "includes/input_read_only.html" with field=form.city %} + {% endif %} + {% if form.state_territory.value is not None %} + {% include "includes/input_read_only.html" with field=form.state_territory %} + {% endif %} + {% if form.zipcode.value is not None %} + {% include "includes/input_read_only.html" with field=form.zipcode %} + {% endif %} + {% endif %} - {% input_with_errors form.address_line2 %} - - {% input_with_errors form.city %} - - {% input_with_errors form.state_territory %} - - {% with add_class="usa-input--small" %} - {% input_with_errors form.zipcode %} - {% endwith %} - - -
{% endblock %} diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index b72f77e21..68a803711 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -26,7 +26,7 @@ def input_with_errors(context, field=None): # noqa: C901 add_group_class: append to input element's surrounding tag's `class` attribute attr_* - adds or replaces any single html attribute for the input add_error_attr_* - like `attr_*` but only if field.errors is not empty - show_edit_button: shows a simple edit button, and adds display-none to the input field. + toggleable_input: shows a simple edit button, and adds display-none to the input field. Example usage: ``` @@ -92,7 +92,7 @@ def input_with_errors(context, field=None): # noqa: C901 elif key == "add_group_class": group_classes.append(value) - elif key == "show_edit_button": + elif key == "toggleable_input": # Hide the primary input field. # Used such that we can toggle it with JS if "display-none" not in classes: diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 3596bf567..c4cdcc2b2 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -37,25 +37,10 @@ class TestPortfolio(WebTest): User.objects.all().delete() super().tearDown() - @less_console_noise_decorator - def test_middleware_does_not_redirect_if_no_permission(self): - """Test that user with no portfolio permission is not redirected when attempting to access home""" - self.app.set_user(self.user.username) - self.user.portfolio = self.portfolio - self.user.save() - self.user.refresh_from_db() - with override_flag("organization_feature", active=True): - # This will redirect the user to the portfolio page. - # Follow implicity checks if our redirect is working. - portfolio_page = self.app.get(reverse("home")) - # Assert that we're on the right page - self.assertNotContains(portfolio_page, self.portfolio.organization_name) - @less_console_noise_decorator def test_middleware_does_not_redirect_if_no_portfolio(self): """Test that user with no assigned portfolio is not redirected when attempting to access home""" self.app.set_user(self.user.username) - self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO] self.user.save() self.user.refresh_from_db() with override_flag("organization_feature", active=True): @@ -67,10 +52,9 @@ class TestPortfolio(WebTest): @less_console_noise_decorator def test_middleware_redirects_to_portfolio_organization_page(self): - """Test that user with VIEW_PORTFOLIO is redirected to portfolio organization page""" + """Test that user with a portfolio is redirected to portfolio organization page""" self.app.set_user(self.user.username) self.user.portfolio = self.portfolio - self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO] self.user.save() self.user.refresh_from_db() with override_flag("organization_feature", active=True): @@ -83,11 +67,10 @@ class TestPortfolio(WebTest): @less_console_noise_decorator def test_middleware_redirects_to_portfolio_domains_page(self): - """Test that user with VIEW_PORTFOLIO and VIEW_ALL_DOMAINS is redirected to portfolio domains page""" + """Test that user with a portfolio and VIEW_ALL_DOMAINS is redirected to portfolio domains page""" self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ - User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, ] self.user.save() @@ -134,20 +117,47 @@ class TestPortfolio(WebTest): self.assertEqual(response.status_code, 403) @less_console_noise_decorator - def test_portfolio_organization_page_403_when_user_not_have_permission(self): - """Test that user without proper permission is not allowed access to portfolio organization page""" + def test_portfolio_organization_page_read_only(self): + """Test that user with a portfolio can access the portfolio organization page, read only""" self.app.set_user(self.user.username) self.user.portfolio = self.portfolio + self.portfolio.city = "Los Angeles" + self.portfolio.save() self.user.save() self.user.refresh_from_db() with override_flag("organization_feature", active=True): - # This will redirect the user to the portfolio page. - # Follow implicity checks if our redirect is working. - response = self.app.get( - reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk}), status=403 - ) - # Assert the response is a 403 Forbidden - self.assertEqual(response.status_code, 403) + response = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk})) + # Assert the response is a 200 + self.assertEqual(response.status_code, 200) + # The label for Federal agency will always be a h4 + self.assertContains(response, '

Federal agency

') + # The read only label for city will be a h4 + self.assertContains(response, '

City

') + self.assertNotContains(response, 'for="id_city"') + self.assertContains(response, '

Los Angeles

') + + @less_console_noise_decorator + def test_portfolio_organization_page_edit_access(self): + """Test that user with a portfolio can access the portfolio organization page, read only""" + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + self.portfolio.city = "Los Angeles" + self.portfolio.save() + self.user.save() + self.user.refresh_from_db() + with override_flag("organization_feature", active=True): + response = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk})) + # Assert the response is a 200 + self.assertEqual(response.status_code, 200) + # The label for Federal agency will always be a h4 + self.assertContains(response, '

Federal agency

') + # The read only label for city will be a h4 + self.assertNotContains(response, '

City

') + self.assertNotContains(response, '

Los Angeles

>') + self.assertContains(response, 'for="id_city"') @less_console_noise_decorator def test_navigation_links_hidden_when_user_not_have_permission(self): @@ -155,7 +165,6 @@ class TestPortfolio(WebTest): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ - User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, ] @@ -176,9 +185,9 @@ class TestPortfolio(WebTest): portfolio_page, reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk}) ) - # reducing portfolio permissions to just VIEW_PORTFOLIO, which should remove domains + # removing non-basic portfolio perms, which should remove domains # and domain requests from nav - self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO] + self.user.portfolio_additional_permissions = [] self.user.save() self.user.refresh_from_db() @@ -194,16 +203,13 @@ class TestPortfolio(WebTest): portfolio_page, reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk}) ) - -class TestPortfolioOrganization(TestPortfolio): - + @less_console_noise_decorator def test_portfolio_org_name(self): """Can load portfolio's org name page.""" with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ - User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ] self.user.save() @@ -214,13 +220,13 @@ class TestPortfolioOrganization(TestPortfolio): page, "The name of your federal agency will be publicly listed as the domain registrant." ) + @less_console_noise_decorator def test_domain_org_name_address_content(self): """Org name and address information appears on the page.""" with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ - User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ] self.user.save() @@ -232,13 +238,13 @@ class TestPortfolioOrganization(TestPortfolio): # Once in the sidenav, once in the main nav, once in the form self.assertContains(page, "Hotel California", count=3) + @less_console_noise_decorator def test_domain_org_name_address_form(self): """Submitting changes works on the org name address page.""" with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ - User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ] self.user.save() diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index abd9648ba..c04044664 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -64,6 +64,9 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin): """Add additional context data to the template.""" context = super().get_context_data(**kwargs) # no need to add portfolio to request context here + + context["has_edit_org_portfolio_permission"] = self.request.user.has_edit_org_portfolio_permission() + context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") context["has_organization_feature_flag"] = flag_is_active(self.request, "organization_feature") return context From f183090e24bd05461fb7c2b1b084914df26bd0e7 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jul 2024 12:04:25 -0400 Subject: [PATCH 19/70] fix unit tests --- src/registrar/tests/test_views_portfolio.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index c4cdcc2b2..f28b10516 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -130,9 +130,9 @@ class TestPortfolio(WebTest): # Assert the response is a 200 self.assertEqual(response.status_code, 200) # The label for Federal agency will always be a h4 - self.assertContains(response, '

Federal agency

') + self.assertContains(response, '

Federal agency

') # The read only label for city will be a h4 - self.assertContains(response, '

City

') + self.assertContains(response, '

City

') self.assertNotContains(response, 'for="id_city"') self.assertContains(response, '

Los Angeles

') @@ -153,9 +153,9 @@ class TestPortfolio(WebTest): # Assert the response is a 200 self.assertEqual(response.status_code, 200) # The label for Federal agency will always be a h4 - self.assertContains(response, '

Federal agency

') + self.assertContains(response, '

Federal agency

') # The read only label for city will be a h4 - self.assertNotContains(response, '

City

') + self.assertNotContains(response, '

City

') self.assertNotContains(response, '

Los Angeles

>') self.assertContains(response, 'for="id_city"') From 44fea22cf976efccc1aaf7be47b544c239282c10 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 30 Jul 2024 09:13:50 -0700 Subject: [PATCH 20/70] Add new developer sandbox 'ad' infrastructure --- .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/manifests/manifest-ad.yaml | 32 ++++++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 4 files changed, 35 insertions(+) create mode 100644 ops/manifests/manifest-ad.yaml 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/registrar/config/settings.py b/src/registrar/config/settings.py index 3da0a104a..8dc2587b9 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -665,6 +665,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", From 6e900bc501cf00238b006fff9719f54de287bc13 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jul 2024 12:35:17 -0400 Subject: [PATCH 21/70] fix unit tests --- src/registrar/tests/test_views_portfolio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index f28b10516..089b710aa 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -134,7 +134,7 @@ class TestPortfolio(WebTest): # The read only label for city will be a h4 self.assertContains(response, '

City

') self.assertNotContains(response, 'for="id_city"') - self.assertContains(response, '

Los Angeles

') + self.assertContains(response, '

Los Angeles

') @less_console_noise_decorator def test_portfolio_organization_page_edit_access(self): @@ -156,7 +156,7 @@ class TestPortfolio(WebTest): self.assertContains(response, '

Federal agency

') # The read only label for city will be a h4 self.assertNotContains(response, '

City

') - self.assertNotContains(response, '

Los Angeles

>') + self.assertNotContains(response, '

Los Angeles

>') self.assertContains(response, 'for="id_city"') @less_console_noise_decorator From 53e1fe96931b584b824bf474c7c30126681d4614 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 13:31:38 -0600 Subject: [PATCH 22/70] update code for extracting "other contact" details --- src/registrar/assets/js/copy-summary.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index de15596d2..2416b07bb 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -34,16 +34,16 @@ document.addEventListener('DOMContentLoaded', function() { // 2 - Iterate through contact details and assemble html for summary let otherContactsSummary = "" // Get the table rows of contact details - const otherContactsTable = document.querySelector('.form-row.field-other_contacts table tbody'); - if (otherContactsTable) { - const otherContactsRows = otherContactsTable.querySelectorAll('tr'); - const bulletList = document.createElement('ul'); - 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(); + // Select all contact elements + const contacts = document.querySelectorAll('.dja-detail-list dl'); + + // Iterate through each contact element + contacts.forEach(contact => { + 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'); @@ -51,7 +51,7 @@ document.addEventListener('DOMContentLoaded', function() { bulletList.appendChild(listItem); }); otherContactsSummary += bulletList.outerHTML - } + }); //------ Requested Domains From 6c82ec9dc296438a352890585f1d778ca8647ab4 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 30 Jul 2024 13:33:14 -0700 Subject: [PATCH 23/70] Update for organization to have election --- src/registrar/models/domain_request.py | 8 ++++++++ src/registrar/utility/csv_export.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index a7252e16b..1ff1e501a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -215,6 +215,14 @@ 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 + if not org_name: + return None + + return cls(org_name).label if org_name else None + class OrganizationChoicesVerbose(models.TextChoices): """ Tertiary organization choices diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 5fbd255aa..d852df5db 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -374,8 +374,9 @@ class DomainExport(BaseExport): if first_ready_on is None: first_ready_on = "(blank)" - domain_org_type = model.get("generic_org_type") - human_readable_domain_org_type = DomainRequest.OrganizationChoices.get_org_label(domain_org_type) + # organization_type has generic_org_type AND is_election + domain_org_type = model.get("organization_type") + human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type) domain_federal_type = model.get("federal_type") human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type) domain_type = human_readable_domain_org_type From e446aa9511fa98b3655cbe79ec601c75a89d10e1 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 16:05:15 -0600 Subject: [PATCH 24/70] copy logic updates, icon updates --- src/registrar/assets/js/copy-summary.js | 12 +++++------- .../templates/admin/change_form_object_tools.html | 8 +++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 2416b07bb..178f61185 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -21,9 +21,9 @@ document.addEventListener('DOMContentLoaded', function() { // 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'); - const nameToUrlMap = {}; otherContactLinks.forEach(link => { const name = link.textContent.trim(); const url = link.href; @@ -38,20 +38,20 @@ document.addEventListener('DOMContentLoaded', function() { const contacts = document.querySelectorAll('.dja-detail-list dl'); // Iterate through each contact element + const bulletList = document.createElement('ul'); contacts.forEach(contact => { 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); - }); - otherContactsSummary += bulletList.outerHTML }); + otherContactsSummary += bulletList.outerHTML //------ Requested Domains @@ -115,12 +115,10 @@ document.addEventListener('DOMContentLoaded', function() { 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) }) ]).then(() => { - console.log('Bold text copied to clipboard successfully!'); + console.log('Summary copied to clipboard successfully!'); }).catch(err => { console.error('Failed to copy text: ', err); }); document.body.removeChild(tempElement); - - alert('Summary copied to clipboard!'); }); }); diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index c2d22e9e2..1094f4c86 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 %} @@ -19,7 +20,12 @@ {% if opts.model_name == 'domainrequest' %}
  • - {% translate "Copy request summary" %} + + + + + {% translate "Copy request summary" %} +
  • {% endif %} From 0953e50bb0d01155436f1ec5ab49e971d19cbbe6 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 17:39:02 -0600 Subject: [PATCH 25/70] styling updates --- src/registrar/assets/sass/_theme/_buttons.scss | 5 +++++ src/registrar/templates/admin/change_form_object_tools.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 7fa379c0b..577c9ce29 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -128,6 +128,11 @@ a.withdraw:active { vertical-align: bottom; } +a.historylink .usa-icon { + vertical-align: middle; + margin: 0px; +} + a.usa-button--unstyled:visited { color: color('primary'); } diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 1094f4c86..03c519241 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -22,7 +22,7 @@
  • - + {% translate "Copy request summary" %} From 7f212edda5d61ad0b82ead1df71818369fb4959b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jul 2024 19:58:39 -0400 Subject: [PATCH 26/70] revert some of the tweaks --- ..._user_portfolio_additional_permissions.py} | 17 +------- src/registrar/models/user.py | 14 +++--- src/registrar/tests/test_views_portfolio.py | 43 ++++++++++++++++++- 3 files changed, 51 insertions(+), 23 deletions(-) rename src/registrar/migrations/{0114_alter_user_portfolio_additional_permissions_and_more.py => 0114_alter_user_portfolio_additional_permissions.py} (70%) diff --git a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions_and_more.py b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py similarity index 70% rename from src/registrar/migrations/0114_alter_user_portfolio_additional_permissions_and_more.py rename to src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py index f70c5388c..27f6e699f 100644 --- a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions_and_more.py +++ b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-07-30 02:51 +# Generated by Django 4.2.10 on 2024-07-30 23:58 import django.contrib.postgres.fields from django.db import migrations, models @@ -26,6 +26,7 @@ class Migration(migrations.Migration): ("view_created_requests", "View created requests"), ("edit_requests", "Create and edit requests"), ("edit_portfolio", "Edit organization"), + ("view_portfolio", "View organization"), ], max_length=50, ), @@ -35,18 +36,4 @@ class Migration(migrations.Migration): size=None, ), ), - migrations.AlterField( - model_name="user", - name="portfolio_roles", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - choices=[("organization_admin", "Admin"), ("organization_admin_read_only", "Admin read only")], - max_length=50, - ), - blank=True, - help_text="Select one or more roles.", - null=True, - size=None, - ), - ), ] diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index dd826bc11..48e6dc420 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -64,15 +64,15 @@ class User(AbstractUser): class UserPortfolioRoleChoices(models.TextChoices): """ - Roles make it easier for admins to look at + Roles make it easier for admins to look at groups of users """ ORGANIZATION_ADMIN = "organization_admin", "Admin" ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only" - # ORGANIZATION_MEMBER is an abstract role where user.portfolio is true + ORGANIZATION_MEMBER = "organization_member", "Member" class UserPortfolioPermissionChoices(models.TextChoices): - """ """ + """We test against permissions to manage access""" VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports" VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains" @@ -89,8 +89,8 @@ class User(AbstractUser): VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests" EDIT_REQUESTS = "edit_requests", "Create and edit requests" - # VIEW_PORTFOLIO is an abstract permission that returns true when user.portfolio is true EDIT_PORTFOLIO = "edit_portfolio", "Edit organization" + VIEW_PORTFOLIO = "view_portfolio", "View organization" PORTFOLIO_ROLE_PERMISSIONS = { UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [ @@ -106,6 +106,9 @@ class User(AbstractUser): UserPortfolioPermissionChoices.VIEW_MEMBER, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, ], + UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ + UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + ], } # #### Constants for choice fields #### @@ -278,8 +281,7 @@ class User(AbstractUser): # the methods below are checks for individual portfolio permissions. They are defined here # to make them easier to call elsewhere throughout the application def has_base_portfolio_permission(self): - """Base role/permission, the user is simply linked to a portfolio""" - return self.portfolio is not None + return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO) def has_edit_org_portfolio_permission(self): return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 089b710aa..21b794cf7 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -37,10 +37,25 @@ class TestPortfolio(WebTest): User.objects.all().delete() super().tearDown() + @less_console_noise_decorator + def test_middleware_does_not_redirect_if_no_permission(self): + """Test that user with no portfolio permission is not redirected when attempting to access home""" + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.save() + self.user.refresh_from_db() + with override_flag("organization_feature", active=True): + # This will redirect the user to the portfolio page. + # Follow implicity checks if our redirect is working. + portfolio_page = self.app.get(reverse("home")) + # Assert that we're on the right page + self.assertNotContains(portfolio_page, self.portfolio.organization_name) + @less_console_noise_decorator def test_middleware_does_not_redirect_if_no_portfolio(self): """Test that user with no assigned portfolio is not redirected when attempting to access home""" self.app.set_user(self.user.username) + self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO] self.user.save() self.user.refresh_from_db() with override_flag("organization_feature", active=True): @@ -52,9 +67,10 @@ class TestPortfolio(WebTest): @less_console_noise_decorator def test_middleware_redirects_to_portfolio_organization_page(self): - """Test that user with a portfolio is redirected to portfolio organization page""" + """Test that user with a portfolio and VIEW_PORTFOLIO is redirected to portfolio organization page""" self.app.set_user(self.user.username) self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO] self.user.save() self.user.refresh_from_db() with override_flag("organization_feature", active=True): @@ -67,10 +83,12 @@ class TestPortfolio(WebTest): @less_console_noise_decorator def test_middleware_redirects_to_portfolio_domains_page(self): - """Test that user with a portfolio and VIEW_ALL_DOMAINS is redirected to portfolio domains page""" + """Test that user with a portfolio, VIEW_PORTFOLIO, VIEW_ALL_DOMAINS + is redirected to portfolio domains page""" self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, ] self.user.save() @@ -116,12 +134,29 @@ class TestPortfolio(WebTest): # Assert the response is a 403 Forbidden self.assertEqual(response.status_code, 403) + @less_console_noise_decorator + def test_portfolio_organization_page_403_when_user_not_have_permission(self): + """Test that user without proper permission is not allowed access to portfolio organization page""" + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.save() + self.user.refresh_from_db() + with override_flag("organization_feature", active=True): + # This will redirect the user to the portfolio page. + # Follow implicity checks if our redirect is working. + response = self.app.get( + reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk}), status=403 + ) + # Assert the response is a 403 Forbidden + self.assertEqual(response.status_code, 403) + @less_console_noise_decorator def test_portfolio_organization_page_read_only(self): """Test that user with a portfolio can access the portfolio organization page, read only""" self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.portfolio.city = "Los Angeles" + self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO] self.portfolio.save() self.user.save() self.user.refresh_from_db() @@ -142,6 +177,7 @@ class TestPortfolio(WebTest): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ] self.portfolio.city = "Los Angeles" @@ -210,6 +246,7 @@ class TestPortfolio(WebTest): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ] self.user.save() @@ -227,6 +264,7 @@ class TestPortfolio(WebTest): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ] self.user.save() @@ -245,6 +283,7 @@ class TestPortfolio(WebTest): self.app.set_user(self.user.username) self.user.portfolio = self.portfolio self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ] self.user.save() From 867ccd097e32c29493da3e75241bbe4053b262b9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jul 2024 20:21:41 -0400 Subject: [PATCH 27/70] remove migration --- ...r_user_portfolio_additional_permissions.py | 39 ------------------- src/registrar/models/user.py | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py diff --git a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py deleted file mode 100644 index 27f6e699f..000000000 --- a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.10 on 2024-07-30 23:58 - -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"), - ("edit_domains", "User is a manager on a domain"), - ("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"), - ("edit_portfolio", "Edit organization"), - ("view_portfolio", "View organization"), - ], - max_length=50, - ), - blank=True, - help_text="Select one or more additional permissions.", - null=True, - size=None, - ), - ), - ] diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 48e6dc420..f4d7635cd 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -89,8 +89,8 @@ class User(AbstractUser): VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests" EDIT_REQUESTS = "edit_requests", "Create and edit requests" - EDIT_PORTFOLIO = "edit_portfolio", "Edit organization" VIEW_PORTFOLIO = "view_portfolio", "View organization" + EDIT_PORTFOLIO = "edit_portfolio", "Edit organization" PORTFOLIO_ROLE_PERMISSIONS = { UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [ From 6a94da8a5d96cc0788cb375525ec763bfc3e3373 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jul 2024 20:22:53 -0400 Subject: [PATCH 28/70] cleanup --- src/registrar/models/user.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index f4d7635cd..19097a96e 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -99,12 +99,14 @@ class User(AbstractUser): UserPortfolioPermissionChoices.EDIT_MEMBER, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS, + UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO, ], UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [ UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, UserPortfolioPermissionChoices.VIEW_MEMBER, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, + UserPortfolioPermissionChoices.VIEW_PORTFOLIO, ], UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ UserPortfolioPermissionChoices.VIEW_PORTFOLIO, From 7b56562417f3a5c2eb5b8e8982eabf9c3092860e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 18:45:44 -0600 Subject: [PATCH 29/70] Fixed error with javascript file, more styling --- src/registrar/assets/js/copy-summary.js | 124 ------------- src/registrar/assets/js/get-gov-admin.js | 163 +++++++++++++++++- .../assets/sass/_theme/_buttons.scss | 7 +- src/registrar/assets/sass/_theme/_links.scss | 5 + .../templates/admin/change_form.html | 4 - .../admin/change_form_object_tools.html | 6 +- 6 files changed, 165 insertions(+), 144 deletions(-) delete mode 100644 src/registrar/assets/js/copy-summary.js diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js deleted file mode 100644 index 178f61185..000000000 --- a/src/registrar/assets/js/copy-summary.js +++ /dev/null @@ -1,124 +0,0 @@ - -document.addEventListener('DOMContentLoaded', function() { - document.getElementById('copy-summary-btn').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 = "" - // Get the table rows of contact details - // Select all contact elements - const contacts = document.querySelectorAll('.dja-detail-list dl'); - - // Iterate through each contact element - const bulletList = document.createElement('ul'); - contacts.forEach(contact => { - 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); - }); - 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 summary = `Recommendation:
    ` + - `Organization Type: ${organizationType}
    ` + - `Requested Domain: ${requestedDomain}
    ` + - `Existing website(s): ${existingWebsites.join(', ')}
    ` + - `Rationale:
    ` + - `Alternate Domain(s): ${alternativeDomains.join(', ')}
    ` + - `Submitter: ${submitterInfo}
    ` + - `Senior Official: ${seniorOfficialInfo}
    ` + - `Additional Contact(s): ${otherContactsSummary}
    `; - - // Create a temporary element - let tempElement = document.createElement('div'); - tempElement.innerHTML = summary; - // Append the element to the body - document.body.appendChild(tempElement); - - // Use the Selection and Range APIs to select the element's content - let range = document.createRange(); - range.selectNodeContents(tempElement); - let selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - // Use the Clipboard API to write the selected HTML content to the clipboard - navigator.clipboard.write([ - new ClipboardItem({ - 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) - }) - ]).then(() => { - console.log('Summary copied to clipboard successfully!'); - }).catch(err => { - console.error('Failed to copy text: ', err); - }); - document.body.removeChild(tempElement); - }); -}); diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index d8bc21899..fe25b2feb 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -207,6 +207,7 @@ function addOrRemoveSessionBoolean(name, add){ })(); + /** An IIFE for pages in DjangoAdmin that use a clipboard button */ (function (){ @@ -233,21 +234,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); }); @@ -605,3 +602,155 @@ function initializeWidgetOnList(list, parentId) { } } })(); + + +/** An IIFE for copy summary button (appears in DomainRegistry models) +*/ +(function (){ + const copyButton = document.getElementById('copy-summary-btn'); + + 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 = "" + // Get the table rows of contact details + // Select all contact elements + const contacts = document.querySelectorAll('.dja-detail-list dl'); + + // Iterate through each contact element + const bulletList = document.createElement('ul'); + contacts.forEach(contact => { + 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); + }); + 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 summary = `Recommendation:
    ` + + `Organization Type: ${organizationType}
    ` + + `Requested Domain: ${requestedDomain}
    ` + + `Existing website(s): ${existingWebsites.join(', ')}
    ` + + `Rationale:
    ` + + `Alternate Domain(s): ${alternativeDomains.join(', ')}
    ` + + `Submitter: ${submitterInfo}
    ` + + `Senior Official: ${seniorOfficialInfo}
    ` + + `Additional Contact(s): ${otherContactsSummary}
    `; + + // Create a temporary element + let tempElement = document.createElement('div'); + tempElement.innerHTML = summary; + // Append the element to the body + document.body.appendChild(tempElement); + + // Use the Selection and Range APIs to select the element's content + let range = document.createRange(); + range.selectNodeContents(tempElement); + let selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + // Use the Clipboard API to write the selected HTML content to the clipboard + navigator.clipboard.write([ + new ClipboardItem({ + 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) + }) + ]).then(() => { + // Change the icon to a checkmark on successful copy + let buttonIcon = copyButton.querySelector('.usa-button__clipboard 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); + }); + document.body.removeChild(tempElement); + }); + } +})(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 577c9ce29..d246366d8 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -128,11 +128,6 @@ a.withdraw:active { vertical-align: bottom; } -a.historylink .usa-icon { - vertical-align: middle; - margin: 0px; -} - a.usa-button--unstyled:visited { color: color('primary'); } @@ -218,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..35546face 100644 --- a/src/registrar/assets/sass/_theme/_links.scss +++ b/src/registrar/assets/sass/_theme/_links.scss @@ -15,3 +15,8 @@ margin-right: units(0.5); } } + +.modelLink-icon { + margin-bottom: 2px; + vertical-align: middle; +} \ No newline at end of file diff --git a/src/registrar/templates/admin/change_form.html b/src/registrar/templates/admin/change_form.html index 3efbe554e..f2ac7f2df 100644 --- a/src/registrar/templates/admin/change_form.html +++ b/src/registrar/templates/admin/change_form.html @@ -10,8 +10,4 @@ {% endblock %} {% endif %} -{% endblock %} - -{% block extrahead %} - {% 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 03c519241..ec50f493a 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -16,12 +16,12 @@ {% else %}