From 99fde890175608b272f9304d5bfd258d78f10b83 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Thu, 20 Jun 2024 17:16:40 -0400 Subject: [PATCH 01/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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 a41225be7ebe3500f8af6061887d1d745e5cbf0a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 19 Jul 2024 18:35:55 -0400 Subject: [PATCH 08/67] updated clean_tables to run in batches of 1000 rows --- .../management/commands/clean_tables.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/registrar/management/commands/clean_tables.py b/src/registrar/management/commands/clean_tables.py index f0c51390b..f7bc37df7 100644 --- a/src/registrar/management/commands/clean_tables.py +++ b/src/registrar/management/commands/clean_tables.py @@ -56,14 +56,24 @@ class Command(BaseCommand): self.clean_table(table_name) def clean_table(self, table_name): - """Delete all rows in the given table""" + """Delete all rows in the given table. + + Delete in batches to be able to handle large tables""" try: # Get the model class dynamically model = apps.get_model("registrar", table_name) - # Use a transaction to ensure database integrity - with transaction.atomic(): - model.objects.all().delete() - logger.info(f"Successfully cleaned table {table_name}") + BATCH_SIZE = 1000 + total_deleted = 0 + while True: + pks = list(model.objects.values_list("pk", flat=True)[:BATCH_SIZE]) + if not pks: + break + # Use a transaction to ensure database integrity + with transaction.atomic(): + deleted, _ = model.objects.filter(pk__in=pks).delete() + total_deleted += deleted + logger.debug(f"Deleted {deleted} objects, total deleted: {total_deleted}") + logger.info(f"Successfully cleaned table {table_name}, deleted {total_deleted} rows") except LookupError: logger.error(f"Model for table {table_name} not found.") except Exception as e: From 2ee6aec639932ba028cdecd3049399f1b97499c8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 19 Jul 2024 18:45:59 -0400 Subject: [PATCH 09/67] improved logging --- src/registrar/management/commands/clean_tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/clean_tables.py b/src/registrar/management/commands/clean_tables.py index f7bc37df7..dd84b0b62 100644 --- a/src/registrar/management/commands/clean_tables.py +++ b/src/registrar/management/commands/clean_tables.py @@ -72,7 +72,7 @@ class Command(BaseCommand): with transaction.atomic(): deleted, _ = model.objects.filter(pk__in=pks).delete() total_deleted += deleted - logger.debug(f"Deleted {deleted} objects, total deleted: {total_deleted}") + logger.debug(f"Deleted {deleted} {table_name}s, total deleted: {total_deleted}") logger.info(f"Successfully cleaned table {table_name}, deleted {total_deleted} rows") except LookupError: logger.error(f"Model for table {table_name} not found.") From ffbda787ae300d0a0989c3d8f64d81a835c85b1f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 19 Jul 2024 22:10:42 -0400 Subject: [PATCH 10/67] updated tests --- .../tests/test_management_scripts.py | 113 +++++++++++++----- 1 file changed, 84 insertions(+), 29 deletions(-) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index cfe19b091..8fb754ba0 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -33,6 +33,10 @@ from api.tests.common import less_console_noise_decorator logger = logging.getLogger(__name__) +class CustomDeleteException(Exception): + pass + + class TestPopulateVerificationType(MockEppLib): """Tests for the populate_organization_type script""" @@ -800,36 +804,69 @@ class TestCleanTables(TestCase): @override_settings(IS_PRODUCTION=False) def test_command_cleans_tables(self): """test that the handle method functions properly to clean tables""" - with less_console_noise(): - with patch("django.apps.apps.get_model") as get_model_mock: - model_mock = MagicMock() - get_model_mock.return_value = model_mock - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("clean_tables") + with patch("django.apps.apps.get_model") as get_model_mock: + model_mock = MagicMock() + get_model_mock.return_value = model_mock - table_names = [ - "DomainInformation", - "DomainRequest", - "PublicContact", - "Domain", - "User", - "Contact", - "Website", - "DraftDomain", - "HostIp", - "Host", - ] + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + + # List of pks to be returned in batches, one list for each of 11 tables + pk_batch = [1, 2, 3, 4, 5, 6] + # Create a list of batches with alternating non-empty and empty lists + pk_batches = [pk_batch, []] * 11 + + # Set the side effect of values_list to return different pk batches + # First time values_list is called it returns list of 6 objects to delete; + # Next time values_list is called it returns empty list + def values_list_side_effect(*args, **kwargs): + if args == ("pk",) and kwargs.get("flat", False): + return pk_batches.pop(0) + return [] + + model_mock.objects.values_list.side_effect = values_list_side_effect + # Mock the return value of `delete()` to be (6, ...) + model_mock.objects.filter.return_value.delete.return_value = (6, None) + + call_command("clean_tables") + + table_names = [ + "DomainInformation", + "DomainRequest", + "FederalAgency", + "PublicContact", + "HostIp", + "Host", + "Domain", + "User", + "Contact", + "Website", + "DraftDomain", + ] + + expected_filter_calls = [call(pk__in=[1, 2, 3, 4, 5, 6]) for _ in range(11)] + + actual_filter_calls = [c for c in model_mock.objects.filter.call_args_list if "pk__in" in c[1]] + + try: + # Assert that filter(pk__in=...) was called with expected arguments + self.assertEqual(actual_filter_calls, expected_filter_calls) + + # Check that delete() was called for each batch + for batch in [[1, 2, 3, 4, 5, 6]]: + model_mock.objects.filter(pk__in=batch).delete.assert_called() - # Check that each model's delete method was called for table_name in table_names: get_model_mock.assert_any_call("registrar", table_name) - model_mock.objects.all().delete.assert_called() - - self.logger_mock.info.assert_any_call("Successfully cleaned table DomainInformation") + self.logger_mock.info.assert_any_call( + f"Successfully cleaned table {table_name}, deleted 6 rows" + ) + except AssertionError as e: + print(f"AssertionError: {e}") + raise @override_settings(IS_PRODUCTION=False) def test_command_handles_nonexistent_model(self): @@ -860,15 +897,33 @@ class TestCleanTables(TestCase): with patch("django.apps.apps.get_model") as get_model_mock: model_mock = MagicMock() get_model_mock.return_value = model_mock - model_mock.objects.all().delete.side_effect = Exception("Some error") + + # Mock the values_list so that DomainInformation attempts a delete + pk_batches = [[1, 2, 3, 4, 5, 6], []] + + def values_list_side_effect(*args, **kwargs): + if args == ("pk",) and kwargs.get("flat", False): + return pk_batches.pop(0) + return [] + + model_mock.objects.values_list.side_effect = values_list_side_effect + + # Mock delete to raise a generic exception + model_mock.objects.filter.return_value.delete.side_effect = Exception("Mocked delete exception") with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True, ): - call_command("clean_tables") + with self.assertRaises(Exception) as context: + # Execute the command + call_command("clean_tables") - self.logger_mock.error.assert_any_call("Error cleaning table DomainInformation: Some error") + # Check the exception message + self.assertEqual(str(context.exception), "Custom delete error") + + # Assert that delete was called + model_mock.objects.filter.return_value.delete.assert_called() class TestExportTables(MockEppLib): From bc334bd41fe276eb386753d931a7e284fc44567c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 22 Jul 2024 16:25:14 -0600 Subject: [PATCH 11/67] 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 12/67] 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 13/67] 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 14/67] 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 15/67] 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 16/67] 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 658e7c98a77bb3e8f2b6dd054eb317dc2433db51 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 24 Jul 2024 22:28:34 -0400 Subject: [PATCH 17/67] initial working code --- src/registrar/assets/js/get-gov.js | 11 ++++-- src/registrar/models/user.py | 29 ++++----------- .../templates/includes/domains_table.html | 3 ++ src/registrar/tests/test_models.py | 9 ----- src/registrar/views/domains_json.py | 35 +++++++++++++++---- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index f83966756..bec7e861e 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1141,6 +1141,8 @@ document.addEventListener('DOMContentLoaded', function() { const statusIndicator = document.querySelector('.domain__filter-indicator'); const statusToggle = document.querySelector('.usa-button--filter'); const noPortfolioFlag = document.getElementById('no-portfolio-js-flag'); + const portfolioElement = document.getElementById('portfolio-js-value'); + const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; /** * Loads rows in the domains list, as well as updates pagination around the domains list @@ -1150,10 +1152,15 @@ document.addEventListener('DOMContentLoaded', function() { * @param {*} order - the sort order {asc, desc} * @param {*} scroll - control for the scrollToElement functionality * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id */ - function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm) { + function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) { // fetch json of page of domains, given params - fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`) + let url = `/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}` + if (portfolio) + url += `&portfolio=${portfolio}` + + fetch(url) .then(response => response.json()) .then(data => { if (data.error) { diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index b135e30c7..b1c9473db 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -76,11 +76,6 @@ class User(AbstractUser): VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports" VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains" - # EDIT_DOMAINS is really self.domains. We add is hear and leverage it in has_permission - # so we have one way to test for portfolio and domain edit permissions - # Do we need to check for portfolio domains specifically? - # NOTE: A user on an org can currently invite a user outside the org - EDIT_DOMAINS = "edit_domains", "User is a manager on a domain" VIEW_MEMBER = "view_member", "View members" EDIT_MEMBER = "edit_member", "Create and edit members" @@ -268,11 +263,6 @@ class User(AbstractUser): def _has_portfolio_permission(self, portfolio_permission): """The views should only call this function when testing for perms and not rely on roles.""" - # EDIT_DOMAINS === user is a manager on a domain (has UserDomainRole) - # NOTE: Should we check whether the domain is in the portfolio? - if portfolio_permission == self.UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists(): - return True - if not self.portfolio: return False @@ -286,21 +276,14 @@ class User(AbstractUser): return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO) def has_domains_portfolio_permission(self): - return ( - self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS) - or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS) - # or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS) - ) - - def has_edit_domains_portfolio_permission(self): - return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS) + return self._has_portfolio_permission( + User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS + ) or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS) def has_domain_requests_portfolio_permission(self): - return ( - self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS) - or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS) - # or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_REQUESTS) - ) + return self._has_portfolio_permission( + User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS + ) or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS) @classmethod def needs_identity_verification(cls, email, uuid): diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 3a7aee80b..b7ec4d3f3 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -8,6 +8,9 @@

Domains

+ {% else %} + + {% endif %}
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 8daf15933..9f2872f5d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1221,7 +1221,6 @@ class TestUser(TestCase): 1. Returns False when a user does not have a portfolio 2. Returns True when user has direct permission 3. Returns True when user has permission through a role - 4. Returns True EDIT_DOMAINS when user does not have the perm but has UserDomainRole Note: This tests _get_portfolio_permissions as a side effect """ @@ -1233,11 +1232,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertFalse(user_can_view_all_domains) self.assertFalse(user_can_view_all_requests) - self.assertFalse(user_can_edit_domains) self.user.portfolio = portfolio self.user.save() @@ -1245,11 +1242,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertTrue(user_can_view_all_domains) self.assertFalse(user_can_view_all_requests) - self.assertFalse(user_can_edit_domains) self.user.portfolio_roles = [User.UserPortfolioRoleChoices.ORGANIZATION_ADMIN] self.user.save() @@ -1257,11 +1252,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_requests) - self.assertFalse(user_can_edit_domains) UserDomainRole.objects.all().get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER @@ -1269,11 +1262,9 @@ class TestUser(TestCase): user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() - user_can_edit_domains = self.user.has_edit_domains_portfolio_permission() self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_requests) - self.assertTrue(user_can_edit_domains) Portfolio.objects.all().delete() diff --git a/src/registrar/views/domains_json.py b/src/registrar/views/domains_json.py index 3b3cae2c7..d4c09d808 100644 --- a/src/registrar/views/domains_json.py +++ b/src/registrar/views/domains_json.py @@ -6,6 +6,8 @@ from django.contrib.auth.decorators import login_required from django.urls import reverse from django.db.models import Q +from registrar.models.domain_information import DomainInformation + logger = logging.getLogger(__name__) @@ -14,10 +16,9 @@ def get_domains_json(request): """Given the current request, get all domains that are associated with the UserDomainRole object""" - user_domain_roles = UserDomainRole.objects.filter(user=request.user).select_related("domain_info__sub_organization") - domain_ids = user_domain_roles.values_list("domain_id", flat=True) + domain_ids = get_domain_ids_from_request(request) - objects = Domain.objects.filter(id__in=domain_ids) + objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization") unfiltered_total = objects.count() objects = apply_search(objects, request) @@ -28,7 +29,7 @@ def get_domains_json(request): page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - domains = [serialize_domain(domain) for domain in page_obj.object_list] + domains = [serialize_domain(domain, request.user) for domain in page_obj.object_list] return JsonResponse( { @@ -43,6 +44,21 @@ def get_domains_json(request): ) +def get_domain_ids_from_request(request): + """Get domain ids from request. + + If portfolio specified, return domain ids associated with portfolio. + Otherwise, return domain ids associated with request.user. + """ + portfolio = request.GET.get("portfolio") + if portfolio: + domain_infos = DomainInformation.objects.filter(portfolio=portfolio) + return domain_infos.values_list("domain_id", flat=True) + else: + user_domain_roles = UserDomainRole.objects.filter(user=request.user) + return user_domain_roles.values_list("domain_id", flat=True) + + def apply_search(queryset, request): search_term = request.GET.get("search_term") if search_term: @@ -94,7 +110,7 @@ def apply_sorting(queryset, request): return queryset.order_by(sort_by) -def serialize_domain(domain): +def serialize_domain(domain, user): suborganization_name = None try: domain_info = domain.domain_info @@ -106,6 +122,9 @@ def serialize_domain(domain): domain_info = None logger.debug(f"Issue in domains_json: We could not find domain_info for {domain}") + # Check if there is a UserDomainRole for this domain and user + user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists() + return { "id": domain.id, "name": domain.name, @@ -114,7 +133,11 @@ def serialize_domain(domain): "state_display": domain.state_display(), "get_state_help_text": domain.get_state_help_text(), "action_url": reverse("domain", kwargs={"pk": domain.id}), - "action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"), + "action_label": ( + "View" + if not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] + else "Manage" + ), "svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"), "suborganization": suborganization_name, } From ed669cc4c6715702f8794dd884ea5ca7a6f8f788 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:25:57 -0400 Subject: [PATCH 18/67] updated test code --- .../tests/test_views_domains_json.py | 91 ++++++++++++++++++- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views_domains_json.py b/src/registrar/tests/test_views_domains_json.py index 28a7308f5..09a233768 100644 --- a/src/registrar/tests/test_views_domains_json.py +++ b/src/registrar/tests/test_views_domains_json.py @@ -1,4 +1,4 @@ -from registrar.models import UserDomainRole, Domain +from registrar.models import UserDomainRole, Domain, DomainInformation, Portfolio from django.urls import reverse from .test_views import TestWithUser from django_webtest import WebTest # type: ignore @@ -15,16 +15,25 @@ class GetDomainsJsonTest(TestWithUser, WebTest): self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown") self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed") self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready") + self.domain4 = Domain.objects.create(name="example4.com", expiration_date="2024-03-01", state="ready") # Create UserDomainRoles UserDomainRole.objects.create(user=self.user, domain=self.domain1) UserDomainRole.objects.create(user=self.user, domain=self.domain2) UserDomainRole.objects.create(user=self.user, domain=self.domain3) + # Create Portfolio + self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Example org") + + # Add domain3 and domain4 to portfolio + DomainInformation.objects.create(creator=self.user, domain=self.domain3, portfolio=self.portfolio) + DomainInformation.objects.create(creator=self.user, domain=self.domain4, portfolio=self.portfolio) + def tearDown(self): + UserDomainRole.objects.all().delete() + DomainInformation.objects.all().delete() + Portfolio.objects.all().delete() super().tearDown() - UserDomainRole.objects.all().delete() - UserDomainRole.objects.all().delete() @less_console_noise_decorator def test_get_domains_json_unauthenticated(self): @@ -105,6 +114,82 @@ class GetDomainsJsonTest(TestWithUser, WebTest): ) self.assertEqual(svg_icon_expected, svg_icons[i]) + @less_console_noise_decorator + def test_get_domains_json_with_portfolio(self): + """Test that an authenticated user gets the list of 2 domains for portfolio.""" + + response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_next"]) + self.assertFalse(data["has_previous"]) + self.assertEqual(data["num_pages"], 1) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 2) + + # Expected domains + expected_domains = [self.domain3, self.domain4] + + # Extract fields from response + domain_ids = [domain["id"] for domain in data["domains"]] + names = [domain["name"] for domain in data["domains"]] + expiration_dates = [domain["expiration_date"] for domain in data["domains"]] + states = [domain["state"] for domain in data["domains"]] + state_displays = [domain["state_display"] for domain in data["domains"]] + get_state_help_texts = [domain["get_state_help_text"] for domain in data["domains"]] + action_urls = [domain["action_url"] for domain in data["domains"]] + action_labels = [domain["action_label"] for domain in data["domains"]] + svg_icons = [domain["svg_icon"] for domain in data["domains"]] + + # Check fields for each domain + for i, expected_domain in enumerate(expected_domains): + self.assertEqual(expected_domain.id, domain_ids[i]) + self.assertEqual(expected_domain.name, names[i]) + self.assertEqual(expected_domain.expiration_date, expiration_dates[i]) + self.assertEqual(expected_domain.state, states[i]) + + # Parsing the expiration date from string to date + parsed_expiration_date = parse_date(expiration_dates[i]) + expected_domain.expiration_date = parsed_expiration_date + + # Check state_display and get_state_help_text + self.assertEqual(expected_domain.state_display(), state_displays[i]) + self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i]) + + self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i]) + + # Check action_label + user_domain_role_exists = UserDomainRole.objects.filter( + domain_id=expected_domains[i].id, user=self.user + ).exists() + action_label_expected = ( + "View" + if not user_domain_role_exists + or expected_domains[i].state + in [ + Domain.State.DELETED, + Domain.State.ON_HOLD, + ] + else "Manage" + ) + self.assertEqual(action_label_expected, action_labels[i]) + + # Check svg_icon + svg_icon_expected = ( + "visibility" + if expected_domains[i].state + in [ + Domain.State.DELETED, + Domain.State.ON_HOLD, + ] + else "settings" + ) + self.assertEqual(svg_icon_expected, svg_icons[i]) + @less_console_noise_decorator def test_get_domains_json_search(self): """Test search.""" From edb33bba6979f54c2dd8cd01f0ff140570e07d65 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:35:52 -0400 Subject: [PATCH 19/67] cleaned up some comments --- src/registrar/context_processors.py | 1 - src/registrar/templates/includes/domains_table.html | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 06ef07050..9854cf404 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -61,7 +61,6 @@ def add_path_to_context(request): def add_has_profile_feature_flag_to_context(request): return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")} - def portfolio_permissions(request): """Make portfolio permissions for the request user available in global context""" try: diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index b7ec4d3f3..cd9ea372f 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -2,7 +2,6 @@
- {% if portfolio is None %}

Domains

@@ -41,7 +40,6 @@
- {% if portfolio %}
Filter by @@ -145,7 +143,6 @@ Domain name Expires Status - {% if portfolio %} Suborganization {% endif %} From 784b005968ec9ced1406b689911b21dc6018bc14 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:36:14 -0400 Subject: [PATCH 20/67] cleaned up some comments --- src/registrar/templates/includes/domain_requests_table.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index efebd1e28..ad91699ef 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -2,7 +2,6 @@
- {% if portfolio is None %}

Domain requests

From 06a5803bba480d26fbc7dc2f79e2666c53f3e98e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 25 Jul 2024 08:47:52 -0400 Subject: [PATCH 21/67] lint plus migrations --- src/registrar/context_processors.py | 1 + ...r_user_portfolio_additional_permissions.py | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 9854cf404..06ef07050 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -61,6 +61,7 @@ def add_path_to_context(request): def add_has_profile_feature_flag_to_context(request): return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")} + def portfolio_permissions(request): """Make portfolio permissions for the request user available in global context""" try: diff --git a/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py new file mode 100644 index 000000000..55645298f --- /dev/null +++ b/src/registrar/migrations/0114_alter_user_portfolio_additional_permissions.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.10 on 2024-07-25 12:45 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0113_user_portfolio_user_portfolio_additional_permissions_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="portfolio_additional_permissions", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("view_all_domains", "View all domains and domain reports"), + ("view_managed_domains", "View managed domains"), + ("view_member", "View members"), + ("edit_member", "Create and edit members"), + ("view_all_requests", "View all requests"), + ("view_created_requests", "View created requests"), + ("edit_requests", "Create and edit requests"), + ("view_portfolio", "View organization"), + ("edit_portfolio", "Edit organization"), + ], + max_length=50, + ), + blank=True, + help_text="Select one or more additional permissions.", + null=True, + size=None, + ), + ), + ] From b058365673e9a8e1ce62957c3db2e3164303e03b Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 26 Jul 2024 10:44:57 -0700 Subject: [PATCH 22/67] Swap placement --- src/registrar/templates/includes/finish_profile_form.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 88f7a73af..fffb66b6a 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -53,6 +53,10 @@ {% endwith %}
+ {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} + {% input_with_errors form.title %} + {% endwith %} + {% 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 link_href=login_help_url %} @@ -64,10 +68,6 @@ {% endwith %} {% endwith %} - {% with show_edit_button=True show_readonly=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 add_class="usa-input--medium" %} {% input_with_errors form.phone %} From 6823d5a2b022b1f93f4215e112f612874fcb6a98 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 29 Jul 2024 10:52:06 -0500 Subject: [PATCH 23/67] Initial changes --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index d63cf2f94..3c5812235 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -21,6 +21,21 @@ There are several tools we use locally that you will need to have. - [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg) - Alternatively, you can skip this step and [use ssh keys](#setting-up-commit-signing-with-ssh) instead - [ ] Install the [Github CLI](https://cli.github.com/) +Optional +- [ ] Install the Slack Desktop App + +## For Developing on a DHS furnished device + +The following tools much be requested through the DHS IT portal: +- [ ] Docker Community Edition +- [ ] Git +- [ ] VSCode (our preferred editor) +The following tools are optional, but also must be requested through the IT portal: +- [ ] Python 3.10 +- [ ] NodeJS (latest version available) +- [ ] Putty +- [ ] Windows Subsystem for Linux +- [ ] Github Desktop ## Access From bdbc66ec518a7d4a802b77664d6f28fcc52d340e Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:24:17 -0700 Subject: [PATCH 24/67] Initial update to dev onboarding ticket --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 3c5812235..0c241bc85 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -30,7 +30,8 @@ The following tools much be requested through the DHS IT portal: - [ ] Docker Community Edition - [ ] Git - [ ] VSCode (our preferred editor) -The following tools are optional, but also must be requested through the IT portal: + +The following tools are optional, but also can be requested through the DHS IT portal: - [ ] Python 3.10 - [ ] NodeJS (latest version available) - [ ] Putty From 2b0d5e8e718dae5832aced212ef6121c96d662bd Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 29 Jul 2024 11:39:30 -0500 Subject: [PATCH 25/67] Add known issues section. --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 3c5812235..117886b65 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -26,7 +26,7 @@ Optional ## For Developing on a DHS furnished device -The following tools much be requested through the DHS IT portal: +The following tools must be requested through the DHS IT portal: - [ ] Docker Community Edition - [ ] Git - [ ] VSCode (our preferred editor) @@ -139,3 +139,11 @@ Additionally, consider a gpg key manager like Kleopatra if you run into issues w We have three types of environments: stable, staging, and sandbox. Stable (production)and staging (pre-prod) get deployed via tagged release, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable or staging. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer. All automation for setting up a developer sandbox is documented in the scripts for [creating a developer sandbox](../../ops/scripts/create_dev_sandbox.sh) and [removing a developer sandbox](../../ops/scripts/destroy_dev_sandbox.sh). A Cloud.gov organization administrator will have to perform the script in order to create the sandbox. + +# Known Issues + +## SSL Verification Failure +Some developers, especially those using Government Furnished Equipment (GFE), have problems installing python packages due to an SSL verification failure. This happens because GFE has a custom certificate chain installed, but python uses its own certificate bundle. As a result, when pip tries to verify the TLS connection to download a package, it cannot and so the download fails. To resolve this, if you are running locally you can use --use-feature=truststore to direct pip to use the local certificate store. If you are running a docker container, you will need to export the root certificate and pull it into the container. Ask another developer how to do this properly. + +## Checksum Error +There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. \ No newline at end of file From 30182921ce8539c27c3be256a5b9d2f69c6c76b9 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:16:23 -0700 Subject: [PATCH 26/67] Add login and login sandbox tasks --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 84db58ca3..f564da217 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -53,7 +53,12 @@ cf login -a api.fr.cloud.gov --sso **Note:** As mentioned in the [Login documentation](https://developers.login.gov/testing/), the sandbox Login account is different account from your regular, production Login account. If you have not created a Login account for the sandbox before, you will need to create a new account first. -- [ ] Optional- add yourself as a codeowner if desired. See the [Developer readme](https://github.com/cisagov/getgov/blob/main/docs/developer/README.md) for how to do this and what it does. +Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Mark the below checkboxes as complete to indicate you successfully set up the following accounts. +- [ ] Identity sandbox accounts - 1 superuser access account and 1 analyst access account. +- [ ] Login.gov account to access stable + +**Optional** +- [ ] Add yourself as a codeowner if desired. See the [Developer readme](https://github.com/cisagov/getgov/blob/main/docs/developer/README.md) for how to do this and what it does. ### Steps for the onboarder - [ ] Add the onboardee to cloud.gov org (cisa-dotgov) @@ -147,4 +152,4 @@ All automation for setting up a developer sandbox is documented in the scripts f Some developers, especially those using Government Furnished Equipment (GFE), have problems installing python packages due to an SSL verification failure. This happens because GFE has a custom certificate chain installed, but python uses its own certificate bundle. As a result, when pip tries to verify the TLS connection to download a package, it cannot and so the download fails. To resolve this, if you are running locally you can use --use-feature=truststore to direct pip to use the local certificate store. If you are running a docker container, you will need to export the root certificate and pull it into the container. Ask another developer how to do this properly. ## Checksum Error -There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. \ No newline at end of file +There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. From ee402120856ddf450249d30f2e0291b94667d4fd Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:20:56 -0700 Subject: [PATCH 27/67] Update developer-onboarding.md --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index f564da217..fecb10a71 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -53,7 +53,7 @@ cf login -a api.fr.cloud.gov --sso **Note:** As mentioned in the [Login documentation](https://developers.login.gov/testing/), the sandbox Login account is different account from your regular, production Login account. If you have not created a Login account for the sandbox before, you will need to create a new account first. -Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Mark the below checkboxes as complete to indicate you successfully set up the following accounts. +Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Mark the below checkboxes as complete to confirm you successfully set up the following accounts. - [ ] Identity sandbox accounts - 1 superuser access account and 1 analyst access account. - [ ] Login.gov account to access stable From 309227151488b3b4f754773b57e915b59e8c0941 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:23:23 -0700 Subject: [PATCH 28/67] Update developer-onboarding.md --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index fecb10a71..05a1f1cd3 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -53,7 +53,7 @@ cf login -a api.fr.cloud.gov --sso **Note:** As mentioned in the [Login documentation](https://developers.login.gov/testing/), the sandbox Login account is different account from your regular, production Login account. If you have not created a Login account for the sandbox before, you will need to create a new account first. -Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Mark the below checkboxes as complete to confirm you successfully set up the following accounts. +Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Confirm you successfully set up the following accounts. - [ ] Identity sandbox accounts - 1 superuser access account and 1 analyst access account. - [ ] Login.gov account to access stable From 66a5b67b27332482089b112257a3c2e5f2fdf436 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 29 Jul 2024 13:51:03 -0500 Subject: [PATCH 29/67] new format for software --- .../ISSUE_TEMPLATE/developer-onboarding.md | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 84db58ca3..3e25ca085 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -12,31 +12,26 @@ assignees: abroddrick - Onboardee: _GH handle of person being onboarded_ - Onboarder: _GH handle of onboard buddy_ -## Installation +## Installation -There are several tools we use locally that you will need to have. -- [ ] [Install the cf CLI v7](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) for the ability to deploy +There are several tools we use locally that you will need to have. Tools marked with * need to be requested through the DHS IT portal when operating on a DHS device + +- [ ] [Cloudfoundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) - If you are using Windows, installation information can be found [here](https://github.com/cloudfoundry/cli/wiki/V8-CLI-Installation-Guide#installers-and-compressed-binaries) - Alternatively, for Windows, [consider using chocolately](https://community.chocolatey.org/packages/cloudfoundry-cli/7.2.0) -- [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg) - - Alternatively, you can skip this step and [use ssh keys](#setting-up-commit-signing-with-ssh) instead -- [ ] Install the [Github CLI](https://cli.github.com/) -Optional -- [ ] Install the Slack Desktop App - -## For Developing on a DHS furnished device - -The following tools must be requested through the DHS IT portal: -- [ ] Docker Community Edition -- [ ] Git -- [ ] VSCode (our preferred editor) +- [ ] [GPG](https://gnupg.org/download/) + - This may not work on DHS devices. Instead, you can [use ssh keys](#setting-up-commit-signing-with-ssh) instead +- [ ] *Docker Community Edition +- [ ] *Git +- [ ] *VSCode (our preferred editor) +- [ ] *Github Desktop The following tools are optional, but also can be requested through the DHS IT portal: -- [ ] Python 3.10 -- [ ] NodeJS (latest version available) -- [ ] Putty -- [ ] Windows Subsystem for Linux -- [ ] Github Desktop +- [ ] Slack Desktop App +- [ ] *Python 3.10 +- [ ] *NodeJS (latest version available) +- [ ] *Putty +- [ ] *Windows Subsystem for Linux ## Access From eaaeb56b1df657cf471c6cd6df6fbe1c8f5a3c13 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 29 Jul 2024 13:56:22 -0500 Subject: [PATCH 30/67] minor clarity edits --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index ba0deaa70..61420f740 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -14,7 +14,7 @@ assignees: abroddrick ## Installation -There are several tools we use locally that you will need to have. Tools marked with * need to be requested through the DHS IT portal when operating on a DHS device +There are several tools we use locally that you will need to have. - [ ] [Cloudfoundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) - If you are using Windows, installation information can be found [here](https://github.com/cloudfoundry/cli/wiki/V8-CLI-Installation-Guide#installers-and-compressed-binaries) @@ -27,12 +27,15 @@ There are several tools we use locally that you will need to have. Tools marked - [ ] *Github Desktop The following tools are optional, but also can be requested through the DHS IT portal: -- [ ] Slack Desktop App +- [ ] **Slack Desktop App - [ ] *Python 3.10 - [ ] *NodeJS (latest version available) - [ ] *Putty - [ ] *Windows Subsystem for Linux +* -> must be requested through DHS IT portal on DHS devices +** -> can be found in software center on DHS devices + ## Access ### Steps for the onboardee From 45b948ef7137c2f56521450a9a16b0d07fc40a94 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 29 Jul 2024 14:06:15 -0500 Subject: [PATCH 31/67] Add known issue instructions --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 61420f740..8fa830a01 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -144,10 +144,16 @@ We have three types of environments: stable, staging, and sandbox. Stable (produ All automation for setting up a developer sandbox is documented in the scripts for [creating a developer sandbox](../../ops/scripts/create_dev_sandbox.sh) and [removing a developer sandbox](../../ops/scripts/destroy_dev_sandbox.sh). A Cloud.gov organization administrator will have to perform the script in order to create the sandbox. -# Known Issues +## Known Issues -## SSL Verification Failure +### SSL Verification Failure Some developers, especially those using Government Furnished Equipment (GFE), have problems installing python packages due to an SSL verification failure. This happens because GFE has a custom certificate chain installed, but python uses its own certificate bundle. As a result, when pip tries to verify the TLS connection to download a package, it cannot and so the download fails. To resolve this, if you are running locally you can use --use-feature=truststore to direct pip to use the local certificate store. If you are running a docker container, you will need to export the root certificate and pull it into the container. Ask another developer how to do this properly. -## Checksum Error -There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. +### Puppeteer Download Error +When building the node image either individually or with docker compose, there may be an error caused by a node package call puppeteer. This can be resolved by adding `ENV PUPPETEER_SKIP_DOWNLOAD=true` to [node.Dockerfile](../../src/node.Dockerfile) after the COPY command. + +### Checksum Error +There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. In the meantime we have a [workaround](#developing-using-docker) + +## Developing Using Docker +While we have unresolved issues with certain devices, you can pull a pre-built docker image from matthewswspence/getgov-base that comes with all the needed packages installed. To do this, you will need to change the very first line in the main [Dockerfile](../../src/Dockerfile) to `FROM matthewswspence/getgov-base:latest`. Note: this change will need to be reverted before any branch can be merged. Additionally, this will only resolve the [checksum error](#checksum-error), you will still need to resolve any other issues through the listed instructions. We are actively working to resolve this inconvenience. From 44fea22cf976efccc1aaf7be47b544c239282c10 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 30 Jul 2024 09:13:50 -0700 Subject: [PATCH 32/67] 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 3ea9649604240f74c1ea896f219edf4e6a9d663e Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Tue, 30 Jul 2024 13:41:05 -0500 Subject: [PATCH 33/67] add line about cf cli --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 8fa830a01..71240cf8f 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -16,7 +16,7 @@ assignees: abroddrick There are several tools we use locally that you will need to have. -- [ ] [Cloudfoundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) +- [ ] [Cloudfoundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) Note: If you are on Windows the cli will be under `cf8` or `cf7` depending on which version you install. - If you are using Windows, installation information can be found [here](https://github.com/cloudfoundry/cli/wiki/V8-CLI-Installation-Guide#installers-and-compressed-binaries) - Alternatively, for Windows, [consider using chocolately](https://community.chocolatey.org/packages/cloudfoundry-cli/7.2.0) - [ ] [GPG](https://gnupg.org/download/) From 53e1fe96931b584b824bf474c7c30126681d4614 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 13:31:38 -0600 Subject: [PATCH 34/67] 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 35/67] 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 36/67] 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 e316989057df49ae27a8a0c2997681fde6706807 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:39:24 -0700 Subject: [PATCH 37/67] Update .github/ISSUE_TEMPLATE/developer-onboarding.md Remove whitespace Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 71240cf8f..a5aba98e8 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -12,7 +12,7 @@ assignees: abroddrick - Onboardee: _GH handle of person being onboarded_ - Onboarder: _GH handle of onboard buddy_ -## Installation +## Installation There are several tools we use locally that you will need to have. From 6dafe9fa2e53f1cf3665ee2b79f394b4830ab423 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:48:37 -0700 Subject: [PATCH 38/67] Update developer-onboarding.md Format required software to more readable format --- .../ISSUE_TEMPLATE/developer-onboarding.md | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index a5aba98e8..bac8fafd7 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -21,20 +21,21 @@ There are several tools we use locally that you will need to have. - Alternatively, for Windows, [consider using chocolately](https://community.chocolatey.org/packages/cloudfoundry-cli/7.2.0) - [ ] [GPG](https://gnupg.org/download/) - This may not work on DHS devices. Instead, you can [use ssh keys](#setting-up-commit-signing-with-ssh) instead -- [ ] *Docker Community Edition -- [ ] *Git -- [ ] *VSCode (our preferred editor) -- [ ] *Github Desktop +- [ ] Docker Community Edition* +- [ ] Git* +- [ ] VSCode (our preferred editor)* +- [ ] Github Desktop* The following tools are optional, but also can be requested through the DHS IT portal: -- [ ] **Slack Desktop App -- [ ] *Python 3.10 -- [ ] *NodeJS (latest version available) -- [ ] *Putty -- [ ] *Windows Subsystem for Linux +- [ ] Slack Desktop App** +- [ ] Python 3.10* +- [ ] NodeJS (latest version available)* +- [ ] Putty* +- [ ] Windows Subsystem for Linux* -* -> must be requested through DHS IT portal on DHS devices -** -> can be found in software center on DHS devices +\* Must be requested through DHS IT portal on DHS devices + +** Downloadable via DHS Software Center ## Access From a9c15162728e4fb4ef5ac78b1fb7579d829c0a6c Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:49:53 -0700 Subject: [PATCH 39/67] Update .github/ISSUE_TEMPLATE/developer-onboarding.md Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index bac8fafd7..77795ab9e 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -52,7 +52,7 @@ cf login -a api.fr.cloud.gov --sso **Note:** As mentioned in the [Login documentation](https://developers.login.gov/testing/), the sandbox Login account is different account from your regular, production Login account. If you have not created a Login account for the sandbox before, you will need to create a new account first. -Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Confirm you successfully set up the following accounts. +Follow the [.gov onboarding dev setup instructions](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.94jwfwkpkhdx). Confirm you successfully set up the following accounts: - [ ] Identity sandbox accounts - 1 superuser access account and 1 analyst access account. - [ ] Login.gov account to access stable From 5a8842b435ab370ae91b6054ed16a7c8610a4a1c Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:50:04 -0700 Subject: [PATCH 40/67] Update .github/ISSUE_TEMPLATE/developer-onboarding.md Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 77795ab9e..3966e6d41 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -154,7 +154,7 @@ Some developers, especially those using Government Furnished Equipment (GFE), ha When building the node image either individually or with docker compose, there may be an error caused by a node package call puppeteer. This can be resolved by adding `ENV PUPPETEER_SKIP_DOWNLOAD=true` to [node.Dockerfile](../../src/node.Dockerfile) after the COPY command. ### Checksum Error -There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. In the meantime we have a [workaround](#developing-using-docker) +There is an unresolved issue with python package installation that occurs after the above SSL Verification failure has been resolved. It often manifests as a checksum error, where the hash of a download .whl file (python package) does not match the expected value. This appears to be because pythonhosted.org is cutting off download connections to some devices for some packages (the behavior is somewhat inconsistent). We have outstanding issues with PyPA and DHS IT to fix this. In the meantime we have a [workaround](#developing-using-docker). ## Developing Using Docker While we have unresolved issues with certain devices, you can pull a pre-built docker image from matthewswspence/getgov-base that comes with all the needed packages installed. To do this, you will need to change the very first line in the main [Dockerfile](../../src/Dockerfile) to `FROM matthewswspence/getgov-base:latest`. Note: this change will need to be reverted before any branch can be merged. Additionally, this will only resolve the [checksum error](#checksum-error), you will still need to resolve any other issues through the listed instructions. We are actively working to resolve this inconvenience. From 0953e50bb0d01155436f1ec5ab49e971d19cbbe6 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 17:39:02 -0600 Subject: [PATCH 41/67] 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 7b56562417f3a5c2eb5b8e8982eabf9c3092860e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 18:45:44 -0600 Subject: [PATCH 42/67] 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 %}