From 0e6bc6f07f13122dda318a86ca5e85ea59d71ee3 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 5 Feb 2025 06:29:10 -0500 Subject: [PATCH 01/50] added helpers for role and permissions displays in templates --- src/registrar/models/portfolio_invitation.py | 58 ++++++++++++ .../models/user_portfolio_permission.py | 58 ++++++++++++ .../models/utility/portfolio_helper.py | 89 +++++++++++++++++++ .../templates/emails/portfolio_update.txt | 35 ++++++++ .../emails/portfolio_update_subject.txt | 1 + .../includes/member_permissions_summary.html | 30 +------ 6 files changed, 245 insertions(+), 26 deletions(-) create mode 100644 src/registrar/templates/emails/portfolio_update.txt create mode 100644 src/registrar/templates/emails/portfolio_update_subject.txt diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 8feeb0794..dd80f946e 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -9,6 +9,10 @@ from .utility.portfolio_helper import ( UserPortfolioPermissionChoices, UserPortfolioRoleChoices, cleanup_after_portfolio_member_deletion, + get_domain_requests_display, + get_domains_display, + get_members_display, + get_role_display, validate_portfolio_invitation, ) # type: ignore from .utility.time_stamped_model import TimeStampedModel @@ -85,6 +89,60 @@ class PortfolioInvitation(TimeStampedModel): """ return UserPortfolioPermission.get_portfolio_permissions(self.roles, self.additional_permissions) + @property + def role_display(self): + """ + Returns a human-readable display name for the user's role. + + Uses the `get_role_display` function to determine if the user is an "Admin", + "Basic" member, or has no role assigned. + + Returns: + str: The display name of the user's role. + """ + return get_role_display(self.roles) + + @property + def domains_display(self): + """ + Returns a string representation of the user's domain access level. + + Uses the `get_domains_display` function to determine whether the user has + "Viewer, all" access (can view all domains) or "Viewer, limited" access. + + Returns: + str: The display name of the user's domain permissions. + """ + return get_domains_display(self.roles, self.additional_permissions) + + @property + def domain_requests_display(self): + """ + Returns a string representation of the user's access to domain requests. + + Uses the `get_domain_requests_display` function to determine if the user + is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), + or has "No access" to domain requests. + + Returns: + str: The display name of the user's domain request permissions. + """ + return get_domain_requests_display(self.roles, self.additional_permissions) + + @property + def members_display(self): + """ + Returns a string representation of the user's access to managing members. + + Uses the `get_members_display` function to determine if the user is a + "Manager" (can edit members), a "Viewer" (can view members), or has "No access" + to member management. + + Returns: + str: The display name of the user's member management permissions. + """ + return get_members_display(self.roles, self.additional_permissions) + @transition(field="status", source=PortfolioInvitationStatus.INVITED, target=PortfolioInvitationStatus.RETRIEVED) def retrieve(self): """When an invitation is retrieved, create the corresponding permission. diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 11d9c56e3..372715db2 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -6,6 +6,10 @@ from registrar.models.utility.portfolio_helper import ( DomainRequestPermissionDisplay, MemberPermissionDisplay, cleanup_after_portfolio_member_deletion, + get_domain_requests_display, + get_domains_display, + get_members_display, + get_role_display, validate_user_portfolio_permission, ) from .utility.time_stamped_model import TimeStampedModel @@ -185,6 +189,60 @@ class UserPortfolioPermission(TimeStampedModel): # This is the same as portfolio_permissions & common_forbidden_perms. return portfolio_permissions.intersection(common_forbidden_perms) + @property + def role_display(self): + """ + Returns a human-readable display name for the user's role. + + Uses the `get_role_display` function to determine if the user is an "Admin", + "Basic" member, or has no role assigned. + + Returns: + str: The display name of the user's role. + """ + return get_role_display(self.roles) + + @property + def domains_display(self): + """ + Returns a string representation of the user's domain access level. + + Uses the `get_domains_display` function to determine whether the user has + "Viewer, all" access (can view all domains) or "Viewer, limited" access. + + Returns: + str: The display name of the user's domain permissions. + """ + return get_domains_display(self.roles, self.additional_permissions) + + @property + def domain_requests_display(self): + """ + Returns a string representation of the user's access to domain requests. + + Uses the `get_domain_requests_display` function to determine if the user + is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), + or has "No access" to domain requests. + + Returns: + str: The display name of the user's domain request permissions. + """ + return get_domain_requests_display(self.roles, self.additional_permissions) + + @property + def members_display(self): + """ + Returns a string representation of the user's access to managing members. + + Uses the `get_members_display` function to determine if the user is a + "Manager" (can edit members), a "Viewer" (can view members), or has "No access" + to member management. + + Returns: + str: The display name of the user's member management permissions. + """ + return get_members_display(self.roles, self.additional_permissions) + def clean(self): """Extends clean method to perform additional validation, which can raise errors in django admin.""" super().clean() diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 0864bded0..f1f9ef4f2 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -82,6 +82,95 @@ class MemberPermissionDisplay(StrEnum): VIEWER = "Viewer" NONE = "None" +def get_role_display(roles): + """ + Returns a user-friendly display name for a given list of user roles. + + - If the user has the ORGANIZATION_ADMIN role, return "Admin". + - If the user has the ORGANIZATION_MEMBER role, return "Basic". + - If the user has neither role, return "-". + + Args: + roles (list): A list of role strings assigned to the user. + + Returns: + str: The display name for the highest applicable role. + """ + if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in roles: + return "Admin" + elif UserPortfolioRoleChoices.ORGANIZATION_MEMBER in roles: + return "Basic" + else: + return "-" + +def get_domains_display(roles, permissions): + """ + Determines the display name for a user's domain viewing permissions. + + - If the user has the VIEW_ALL_DOMAINS permission, return "Viewer, all". + - Otherwise, return "Viewer, limited". + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's domain viewing access. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions: + return "Viewer, all" + else: + return "Viewer, limited" + +def get_domain_requests_display(roles, permissions): + """ + Determines the display name for a user's domain request permissions. + + - If the user has the EDIT_REQUESTS permission, return "Creator". + - If the user has the VIEW_ALL_REQUESTS permission, return "Viewer". + - Otherwise, return "No access". + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's domain request access level. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.EDIT_REQUESTS in all_permissions: + return "Creator" + elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions: + return "Viewer" + else: + return "No access" + +def get_members_display(roles, permissions): + """ + Determines the display name for a user's member management permissions. + + - If the user has the EDIT_MEMBERS permission, return "Manager". + - If the user has the VIEW_MEMBERS permission, return "Viewer". + - Otherwise, return "No access". + + Args: + roles (list): A list of role strings assigned to the user. + permissions (list): A list of additional permissions assigned to the user. + + Returns: + str: A string representing the user's member management access level. + """ + UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) + if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions: + return "Manager" + elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions: + return "Viewer" + else: + return "No access" def validate_user_portfolio_permission(user_portfolio_permission): """ diff --git a/src/registrar/templates/emails/portfolio_update.txt b/src/registrar/templates/emails/portfolio_update.txt new file mode 100644 index 000000000..aa13a9fb9 --- /dev/null +++ b/src/registrar/templates/emails/portfolio_update.txt @@ -0,0 +1,35 @@ +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %} + +Your permissions were updated in the .gov registrar. + +ORGANIZATION: {{ portfolio.organization_name }} +UPDATED BY: {{ requestor_email }} +UPDATED ON: {{ date }} +YOUR PERMISSIONS: {{ permissions.role_display }} + Domains - {{ permissions.domains_display }} + Domain requests - {{ permissions.domain_requests_display }} + Members - {{ permissions.members_display }} + +Your updated permissions are now active in the .gov registrar . + +---------------------------------------------------------------- + +SOMETHING WRONG? +If you have questions or concerns, reach out to the person who updated your +permissions, or reply to this email. + + +THANK YOU +.Gov helps the public identify official, trusted information. Thank you for using a .gov +domain. + +---------------------------------------------------------------- + +The .gov team +Contact us: +Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency +(CISA) +{% endautoescape %} diff --git a/src/registrar/templates/emails/portfolio_update_subject.txt b/src/registrar/templates/emails/portfolio_update_subject.txt new file mode 100644 index 000000000..2cd806a73 --- /dev/null +++ b/src/registrar/templates/emails/portfolio_update_subject.txt @@ -0,0 +1 @@ +Your permissions were updated in the .gov registrar \ No newline at end of file diff --git a/src/registrar/templates/includes/member_permissions_summary.html b/src/registrar/templates/includes/member_permissions_summary.html index 3a91d16f6..95eca0a7e 100644 --- a/src/registrar/templates/includes/member_permissions_summary.html +++ b/src/registrar/templates/includes/member_permissions_summary.html @@ -1,33 +1,11 @@

Member access

-{% if permissions.roles and 'organization_admin' in permissions.roles %} -

Admin

-{% elif permissions.roles and 'organization_member' in permissions.roles %} -

Basic

-{% else %} -

-{% endif %} +

{{ permissions.role_display }}

Domains

-{% if member_has_view_all_domains_portfolio_permission %} -

Viewer, all

-{% else %} -

Viewer, limited

-{% endif %} +

{{ permissions.domains_display }}

Domain requests

-{% if member_has_edit_request_portfolio_permission %} -

Creator

-{% elif member_has_view_all_requests_portfolio_permission %} -

Viewer

-{% else %} -

No access

-{% endif %} +

{{ permissions.domain_requests_display }}

Members

-{% if member_has_edit_members_portfolio_permission %} -

Manager

-{% elif member_has_view_members_portfolio_permission %} -

Viewer

-{% else %} -

No access

-{% endif %} \ No newline at end of file +

{{ permissions.members_display }}

From 350508f9c12f00576bfea74ebaf4d23506e336d7 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 5 Feb 2025 16:31:22 -0500 Subject: [PATCH 02/50] email notification to portfolio member on permissions update --- src/registrar/forms/portfolio.py | 18 ++++++++++ src/registrar/utility/email_invitations.py | 40 ++++++++++++++++++++++ src/registrar/views/portfolios.py | 7 ++++ 3 files changed, 65 insertions(+) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 2725224f1..d244f9931 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -337,6 +337,24 @@ class BasePortfolioMemberForm(forms.ModelForm): UserPortfolioRoleChoices.ORGANIZATION_ADMIN in previous_roles and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in new_roles ) + + def is_change(self) -> bool: + """ + Determines if the form has changed by comparing the initial data + with the submitted cleaned data. + + Returns: + bool: True if the form has changed, False otherwise. + """ + # Compare role values + previous_roles = set(self.initial.get("roles", [])) + new_roles = set(self.cleaned_data.get("roles", [])) + + # Compare additional permissions values + previous_permissions = set(self.initial.get("additional_permissions", [])) + new_permissions = set(self.cleaned_data.get("additional_permissions", [])) + + return previous_roles != new_roles or previous_permissions != new_permissions class PortfolioMemberForm(BasePortfolioMemberForm): diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index de21b2a61..ed057a41b 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -225,6 +225,46 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent +def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission): + """ + Sends an email notification to a portfolio member when their permissions are updated. + + This function retrieves the requestor's email and sends a templated email to the affected user, + notifying them of changes to their portfolio permissions. + + Args: + requestor (User): The user initiating the permission update. + permissions (UserPortfolioPermission): The updated permissions object containing the affected user + and the portfolio details. + + Returns: + bool: True if the email was sent successfully, False if an EmailSendingError occurred. + + Raises: + MissingEmailError: If the requestor has no email associated with their account. + """ + requestor_email = _get_requestor_email(requestor, portfolio=permissions.portfolio) + try: + send_templated_email( + "emails/portfolio_update.txt", + "emails/portfolio_update_subject.txt", + to_address=permissions.user.email, + context={ + "requested_user": permissions.user, + "portfolio": permissions.portfolio, + "requestor_email": requestor_email, + "permissions": permissions, + } + ) + except EmailSendingError as err: + logger.warning( + "Could not send email organization member update notification to %s " "for portfolio: %s", + permissions.user.email, + permissions.portfolio.organization_name, + exc_info=True, + ) + return False + return True def send_portfolio_admin_addition_emails(email: str, requestor, portfolio: Portfolio): """ diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index c5cc72c59..e2d031fca 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -20,6 +20,7 @@ from registrar.utility.email_invitations import ( send_portfolio_admin_addition_emails, send_portfolio_admin_removal_emails, send_portfolio_invitation_email, + send_portfolio_member_permission_update_email, ) from registrar.utility.errors import MissingEmailError from registrar.utility.enums import DefaultUserValues @@ -212,6 +213,12 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): removing_admin_role_on_self = False if form.is_valid(): try: + if form.is_change(): + if not send_portfolio_member_permission_update_email( + requestor=request.user, + permissions=form.instance + ): + messages.warning(self.request, f"Could not send email notification to {user.email}.") if form.is_change_from_member_to_admin(): if not send_portfolio_admin_addition_emails( email=portfolio_permission.user.email, From cbc9cdbe34dd22630a2aef2bd897bab3f168fb16 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 5 Feb 2025 17:13:02 -0500 Subject: [PATCH 03/50] additional tests for email_invitations --- src/registrar/forms/portfolio.py | 10 +-- src/registrar/models/portfolio_invitation.py | 16 ++-- .../models/user_portfolio_permission.py | 16 ++-- .../models/utility/portfolio_helper.py | 5 ++ src/registrar/tests/test_email_invitations.py | 76 ++++++++++++++++++- src/registrar/utility/email_invitations.py | 16 ++-- src/registrar/views/portfolios.py | 3 +- 7 files changed, 111 insertions(+), 31 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index d244f9931..a81f9d1e7 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -337,21 +337,21 @@ class BasePortfolioMemberForm(forms.ModelForm): UserPortfolioRoleChoices.ORGANIZATION_ADMIN in previous_roles and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in new_roles ) - + def is_change(self) -> bool: """ - Determines if the form has changed by comparing the initial data + Determines if the form has changed by comparing the initial data with the submitted cleaned data. - + Returns: bool: True if the form has changed, False otherwise. """ # Compare role values - previous_roles = set(self.initial.get("roles", [])) + previous_roles = set(self.initial.get("roles", [])) new_roles = set(self.cleaned_data.get("roles", [])) # Compare additional permissions values - previous_permissions = set(self.initial.get("additional_permissions", [])) + previous_permissions = set(self.initial.get("additional_permissions", [])) new_permissions = set(self.cleaned_data.get("additional_permissions", [])) return previous_roles != new_roles or previous_permissions != new_permissions diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index dd80f946e..045cf2de4 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -101,41 +101,41 @@ class PortfolioInvitation(TimeStampedModel): str: The display name of the user's role. """ return get_role_display(self.roles) - + @property def domains_display(self): """ Returns a string representation of the user's domain access level. - Uses the `get_domains_display` function to determine whether the user has + Uses the `get_domains_display` function to determine whether the user has "Viewer, all" access (can view all domains) or "Viewer, limited" access. Returns: str: The display name of the user's domain permissions. """ return get_domains_display(self.roles, self.additional_permissions) - + @property def domain_requests_display(self): """ Returns a string representation of the user's access to domain requests. - Uses the `get_domain_requests_display` function to determine if the user - is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), + Uses the `get_domain_requests_display` function to determine if the user + is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), or has "No access" to domain requests. Returns: str: The display name of the user's domain request permissions. """ return get_domain_requests_display(self.roles, self.additional_permissions) - + @property def members_display(self): """ Returns a string representation of the user's access to managing members. - Uses the `get_members_display` function to determine if the user is a - "Manager" (can edit members), a "Viewer" (can view members), or has "No access" + Uses the `get_members_display` function to determine if the user is a + "Manager" (can edit members), a "Viewer" (can view members), or has "No access" to member management. Returns: diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 372715db2..7daa3bd11 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -201,41 +201,41 @@ class UserPortfolioPermission(TimeStampedModel): str: The display name of the user's role. """ return get_role_display(self.roles) - + @property def domains_display(self): """ Returns a string representation of the user's domain access level. - Uses the `get_domains_display` function to determine whether the user has + Uses the `get_domains_display` function to determine whether the user has "Viewer, all" access (can view all domains) or "Viewer, limited" access. Returns: str: The display name of the user's domain permissions. """ return get_domains_display(self.roles, self.additional_permissions) - + @property def domain_requests_display(self): """ Returns a string representation of the user's access to domain requests. - Uses the `get_domain_requests_display` function to determine if the user - is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), + Uses the `get_domain_requests_display` function to determine if the user + is a "Creator" (can create and edit requests), a "Viewer" (can only view requests), or has "No access" to domain requests. Returns: str: The display name of the user's domain request permissions. """ return get_domain_requests_display(self.roles, self.additional_permissions) - + @property def members_display(self): """ Returns a string representation of the user's access to managing members. - Uses the `get_members_display` function to determine if the user is a - "Manager" (can edit members), a "Viewer" (can view members), or has "No access" + Uses the `get_members_display` function to determine if the user is a + "Manager" (can edit members), a "Viewer" (can view members), or has "No access" to member management. Returns: diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index f1f9ef4f2..67e546db8 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -82,6 +82,7 @@ class MemberPermissionDisplay(StrEnum): VIEWER = "Viewer" NONE = "None" + def get_role_display(roles): """ Returns a user-friendly display name for a given list of user roles. @@ -103,6 +104,7 @@ def get_role_display(roles): else: return "-" + def get_domains_display(roles, permissions): """ Determines the display name for a user's domain viewing permissions. @@ -124,6 +126,7 @@ def get_domains_display(roles, permissions): else: return "Viewer, limited" + def get_domain_requests_display(roles, permissions): """ Determines the display name for a user's domain request permissions. @@ -148,6 +151,7 @@ def get_domain_requests_display(roles, permissions): else: return "No access" + def get_members_display(roles, permissions): """ Determines the display name for a user's member management permissions. @@ -172,6 +176,7 @@ def get_members_display(roles, permissions): else: return "No access" + def validate_user_portfolio_permission(user_portfolio_permission): """ Validates a UserPortfolioPermission instance. Located in portfolio_helper to avoid circular imports diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index 77a8c402f..20ac4a565 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -16,6 +16,7 @@ from registrar.utility.email_invitations import ( send_portfolio_admin_addition_emails, send_portfolio_admin_removal_emails, send_portfolio_invitation_email, + send_portfolio_member_permission_update_email, ) from api.tests.common import less_console_noise_decorator @@ -522,7 +523,6 @@ class PortfolioInvitationEmailTests(unittest.TestCase): "registrar.utility.email_invitations._get_requestor_email", side_effect=MissingEmailError("Requestor has no email"), ) - @less_console_noise_decorator def test_send_portfolio_invitation_email_missing_requestor_email(self, mock_get_email): """Test when requestor has no email""" is_admin_invitation = False @@ -888,3 +888,77 @@ class SendPortfolioAdminRemovalEmailsTests(unittest.TestCase): mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio) mock_send_removal_emails.assert_called_once_with(self.email, self.requestor.email, self.portfolio) self.assertFalse(result) + + +class TestSendPortfolioMemberPermissionUpdateEmail(unittest.TestCase): + """Unit tests for send_portfolio_member_permission_update_email function.""" + + @patch("registrar.utility.email_invitations.send_templated_email") + @patch("registrar.utility.email_invitations._get_requestor_email") + def test_send_email_success(self, mock_get_requestor_email, mock_send_email): + """Test that the email is sent successfully when there are no errors.""" + # Mock data + requestor = MagicMock() + permissions = MagicMock(spec=UserPortfolioPermission) + permissions.user.email = "user@example.com" + permissions.portfolio.organization_name = "Test Portfolio" + + mock_get_requestor_email.return_value = "requestor@example.com" + + # Call function + result = send_portfolio_member_permission_update_email(requestor, permissions) + + # Assertions + mock_get_requestor_email.assert_called_once_with(requestor, portfolio=permissions.portfolio) + mock_send_email.assert_called_once_with( + "emails/portfolio_update.txt", + "emails/portfolio_update_subject.txt", + to_address="user@example.com", + context={ + "requested_user": permissions.user, + "portfolio": permissions.portfolio, + "requestor_email": "requestor@example.com", + "permissions": permissions, + }, + ) + self.assertTrue(result) + + @patch("registrar.utility.email_invitations.send_templated_email", side_effect=EmailSendingError("Email failed")) + @patch("registrar.utility.email_invitations._get_requestor_email") + @patch("registrar.utility.email_invitations.logger") + def test_send_email_failure(self, mock_logger, mock_get_requestor_email, mock_send_email): + """Test that the function returns False and logs an error when email sending fails.""" + # Mock data + requestor = MagicMock() + permissions = MagicMock(spec=UserPortfolioPermission) + permissions.user.email = "user@example.com" + permissions.portfolio.organization_name = "Test Portfolio" + + mock_get_requestor_email.return_value = "requestor@example.com" + + # Call function + result = send_portfolio_member_permission_update_email(requestor, permissions) + + # Assertions + mock_logger.warning.assert_called_once_with( + "Could not send email organization member update notification to %s for portfolio: %s", + permissions.user.email, + permissions.portfolio.organization_name, + exc_info=True, + ) + self.assertFalse(result) + + @patch("registrar.utility.email_invitations._get_requestor_email", side_effect=Exception("Unexpected error")) + @patch("registrar.utility.email_invitations.logger") + def test_requestor_email_retrieval_failure(self, mock_logger, mock_get_requestor_email): + """Test that an exception in retrieving requestor email is logged.""" + # Mock data + requestor = MagicMock() + permissions = MagicMock(spec=UserPortfolioPermission) + + # Call function + with self.assertRaises(Exception): + send_portfolio_member_permission_update_email(requestor, permissions) + + # Assertions + mock_logger.warning.assert_not_called() # Function should fail before logging email failure diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index ed057a41b..d206bf279 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -225,6 +225,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent + def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission): """ Sends an email notification to a portfolio member when their permissions are updated. @@ -234,7 +235,7 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo Args: requestor (User): The user initiating the permission update. - permissions (UserPortfolioPermission): The updated permissions object containing the affected user + permissions (UserPortfolioPermission): The updated permissions object containing the affected user and the portfolio details. Returns: @@ -254,18 +255,19 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo "portfolio": permissions.portfolio, "requestor_email": requestor_email, "permissions": permissions, - } + }, ) - except EmailSendingError as err: + except EmailSendingError: logger.warning( "Could not send email organization member update notification to %s " "for portfolio: %s", - permissions.user.email, - permissions.portfolio.organization_name, - exc_info=True, - ) + permissions.user.email, + permissions.portfolio.organization_name, + exc_info=True, + ) return False return True + def send_portfolio_admin_addition_emails(email: str, requestor, portfolio: Portfolio): """ Notifies all portfolio admins of the provided portfolio of a newly invited portfolio admin diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index e2d031fca..d63b5964e 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -215,8 +215,7 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): try: if form.is_change(): if not send_portfolio_member_permission_update_email( - requestor=request.user, - permissions=form.instance + requestor=request.user, permissions=form.instance ): messages.warning(self.request, f"Could not send email notification to {user.email}.") if form.is_change_from_member_to_admin(): From f5ef2002169a68be0beff835b11729e1b50800bf Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 5 Feb 2025 18:27:16 -0500 Subject: [PATCH 04/50] updated tests --- src/registrar/forms/portfolio.py | 4 +- src/registrar/tests/test_views_portfolio.py | 113 ++++++++++++++++---- 2 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index a81f9d1e7..3a9074b2d 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -351,8 +351,8 @@ class BasePortfolioMemberForm(forms.ModelForm): new_roles = set(self.cleaned_data.get("roles", [])) # Compare additional permissions values - previous_permissions = set(self.initial.get("additional_permissions", [])) - new_permissions = set(self.cleaned_data.get("additional_permissions", [])) + previous_permissions = set(self.initial.get("additional_permissions") or []) + new_permissions = set(self.cleaned_data.get("additional_permissions") or []) return previous_roles != new_roles or previous_permissions != new_permissions diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 0c7c56e74..65e0350ee 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -3891,7 +3891,10 @@ class TestPortfolioMemberEditView(WebTest): @override_flag("organization_members", active=True) @patch("registrar.views.portfolios.send_portfolio_admin_addition_emails") @patch("registrar.views.portfolios.send_portfolio_admin_removal_emails") - def test_edit_member_permissions_basic_to_admin(self, mock_send_removal_emails, mock_send_addition_emails): + @patch("registrar.views.portfolios.send_portfolio_member_permission_update_email") + def test_edit_member_permissions_basic_to_admin( + self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails + ): """Tests converting a basic member to admin with full permissions.""" self.client.force_login(self.user) @@ -3906,6 +3909,7 @@ class TestPortfolioMemberEditView(WebTest): # return indicator that notification emails sent successfully mock_send_addition_emails.return_value = True + mock_send_update_email.return_value = True response = self.client.post( reverse("member-permissions", kwargs={"pk": basic_permission.id}), @@ -3925,6 +3929,8 @@ class TestPortfolioMemberEditView(WebTest): mock_send_addition_emails.assert_called_once() # assert removal emails are not sent mock_send_removal_emails.assert_not_called() + # assert update email sent + mock_send_update_email.assert_called_once() # Get the arguments passed to send_portfolio_admin_addition_emails _, called_kwargs = mock_send_addition_emails.call_args @@ -3934,14 +3940,22 @@ class TestPortfolioMemberEditView(WebTest): self.assertEqual(called_kwargs["requestor"], self.user) self.assertEqual(called_kwargs["portfolio"], self.portfolio) + # Get the arguments passed to send_portfolio_member_permission_update_email + _, called_kwargs = mock_send_update_email.call_args + + # Assert the update notification email content + self.assertEqual(called_kwargs["requestor"], self.user) + self.assertEqual(called_kwargs["permissions"], basic_permission) + @less_console_noise_decorator @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @patch("django.contrib.messages.warning") @patch("registrar.views.portfolios.send_portfolio_admin_addition_emails") @patch("registrar.views.portfolios.send_portfolio_admin_removal_emails") + @patch("registrar.views.portfolios.send_portfolio_member_permission_update_email") def test_edit_member_permissions_basic_to_admin_notification_fails( - self, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning + self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning ): """Tests converting a basic member to admin with full permissions. Handle when notification emails fail to send.""" @@ -3958,6 +3972,7 @@ class TestPortfolioMemberEditView(WebTest): # At least one notification email failed to send mock_send_addition_emails.return_value = False + mock_send_update_email.return_value = False response = self.client.post( reverse("member-permissions", kwargs={"pk": basic_permission.id}), @@ -3977,6 +3992,8 @@ class TestPortfolioMemberEditView(WebTest): mock_send_addition_emails.assert_called_once() # assert no removal emails are sent mock_send_removal_emails.assert_not_called() + # assert update email sent + mock_send_update_email.assert_called_once() # Get the arguments passed to send_portfolio_admin_addition_emails _, called_kwargs = mock_send_addition_emails.call_args @@ -3986,18 +4003,32 @@ class TestPortfolioMemberEditView(WebTest): self.assertEqual(called_kwargs["requestor"], self.user) self.assertEqual(called_kwargs["portfolio"], self.portfolio) - # Assert warning message is called correctly - mock_messages_warning.assert_called_once() - warning_args, _ = mock_messages_warning.call_args - self.assertIsInstance(warning_args[0], WSGIRequest) - self.assertEqual(warning_args[1], "Could not send email notification to existing organization admins.") + # Get the arguments passed to send_portfolio_member_permission_update_email + _, called_kwargs = mock_send_update_email.call_args + + # Assert the update notification email content + self.assertEqual(called_kwargs["requestor"], self.user) + self.assertEqual(called_kwargs["permissions"], basic_permission) + + # Assert that messages.warning is called twice + self.assertEqual(mock_messages_warning.call_count, 2) + + # Extract the actual messages sent + warning_messages = [call_args[0][1] for call_args in mock_messages_warning.call_args_list] + + # Check for the expected messages + self.assertIn("Could not send email notification to existing organization admins.", warning_messages) + self.assertIn(f"Could not send email notification to {basic_member.email}.", warning_messages) @less_console_noise_decorator @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @patch("registrar.views.portfolios.send_portfolio_admin_addition_emails") @patch("registrar.views.portfolios.send_portfolio_admin_removal_emails") - def test_edit_member_permissions_admin_to_admin(self, mock_send_removal_emails, mock_send_addition_emails): + @patch("registrar.views.portfolios.send_portfolio_member_permission_update_email") + def test_edit_member_permissions_admin_to_admin( + self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails + ): """Tests updating an admin without changing permissions.""" self.client.force_login(self.user) @@ -4007,6 +4038,7 @@ class TestPortfolioMemberEditView(WebTest): user=admin_member, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + additional_permissions=[], ) response = self.client.post( @@ -4019,16 +4051,20 @@ class TestPortfolioMemberEditView(WebTest): # Verify redirect and success message self.assertEqual(response.status_code, 302) - # assert addition and removal emails are not sent to portfolio admins + # assert update, addition and removal emails are not sent to portfolio admins mock_send_addition_emails.assert_not_called() mock_send_removal_emails.assert_not_called() + mock_send_update_email.assert_not_called() @less_console_noise_decorator @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @patch("registrar.views.portfolios.send_portfolio_admin_addition_emails") @patch("registrar.views.portfolios.send_portfolio_admin_removal_emails") - def test_edit_member_permissions_basic_to_basic(self, mock_send_removal_emails, mock_send_addition_emails): + @patch("registrar.views.portfolios.send_portfolio_member_permission_update_email") + def test_edit_member_permissions_basic_to_basic( + self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails + ): """Tests updating an admin without changing permissions.""" self.client.force_login(self.user) @@ -4041,6 +4077,8 @@ class TestPortfolioMemberEditView(WebTest): additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], ) + mock_send_update_email.return_value = True + response = self.client.post( reverse("member-permissions", kwargs={"pk": basic_permission.id}), { @@ -4057,13 +4095,25 @@ class TestPortfolioMemberEditView(WebTest): # assert addition and removal emails are not sent to portfolio admins mock_send_addition_emails.assert_not_called() mock_send_removal_emails.assert_not_called() + # assert update email is sent to updated member + mock_send_update_email.assert_called_once() + + # Get the arguments passed to send_portfolio_member_permission_update_email + _, called_kwargs = mock_send_update_email.call_args + + # Assert the email content + self.assertEqual(called_kwargs["requestor"], self.user) + self.assertEqual(called_kwargs["permissions"], basic_permission) @less_console_noise_decorator @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @patch("registrar.views.portfolios.send_portfolio_admin_addition_emails") @patch("registrar.views.portfolios.send_portfolio_admin_removal_emails") - def test_edit_member_permissions_admin_to_basic(self, mock_send_removal_emails, mock_send_addition_emails): + @patch("registrar.views.portfolios.send_portfolio_member_permission_update_email") + def test_edit_member_permissions_admin_to_basic( + self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails + ): """Tests converting an admin to basic member.""" self.client.force_login(self.user) @@ -4074,8 +4124,9 @@ class TestPortfolioMemberEditView(WebTest): portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], ) - + print(admin_permission) mock_send_removal_emails.return_value = True + mock_send_update_email.return_value = True response = self.client.post( reverse("member-permissions", kwargs={"pk": admin_permission.id}), @@ -4094,7 +4145,8 @@ class TestPortfolioMemberEditView(WebTest): admin_permission.refresh_from_db() self.assertEqual(admin_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]) - # assert removal emails are sent to portfolio admins + # assert removal emails and update email are sent to portfolio admins + mock_send_update_email.assert_called_once() mock_send_addition_emails.assert_not_called() mock_send_removal_emails.assert_called_once() @@ -4106,14 +4158,22 @@ class TestPortfolioMemberEditView(WebTest): self.assertEqual(called_kwargs["requestor"], self.user) self.assertEqual(called_kwargs["portfolio"], self.portfolio) + # Get the arguments passed to send_portfolio_member_permission_update_email + _, called_kwargs = mock_send_update_email.call_args + + # Assert the email content + self.assertEqual(called_kwargs["requestor"], self.user) + self.assertEqual(called_kwargs["permissions"], admin_permission) + @less_console_noise_decorator @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @patch("django.contrib.messages.warning") @patch("registrar.views.portfolios.send_portfolio_admin_addition_emails") @patch("registrar.views.portfolios.send_portfolio_admin_removal_emails") + @patch("registrar.views.portfolios.send_portfolio_member_permission_update_email") def test_edit_member_permissions_admin_to_basic_notification_fails( - self, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning + self, mock_send_update_email, mock_send_removal_emails, mock_send_addition_emails, mock_messages_warning ): """Tests converting an admin to basic member.""" self.client.force_login(self.user) @@ -4129,6 +4189,7 @@ class TestPortfolioMemberEditView(WebTest): # False return indicates that at least one notification email failed to send mock_send_removal_emails.return_value = False + mock_send_update_email.return_value = False response = self.client.post( reverse("member-permissions", kwargs={"pk": admin_permission.id}), @@ -4147,9 +4208,10 @@ class TestPortfolioMemberEditView(WebTest): admin_permission.refresh_from_db() self.assertEqual(admin_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]) - # assert removal emails are sent to portfolio admins + # assert update email and removal emails are sent to portfolio admins mock_send_addition_emails.assert_not_called() mock_send_removal_emails.assert_called_once() + mock_send_update_email.assert_called_once() # Get the arguments passed to send_portfolio_admin_removal_emails _, called_kwargs = mock_send_removal_emails.call_args @@ -4159,11 +4221,22 @@ class TestPortfolioMemberEditView(WebTest): self.assertEqual(called_kwargs["requestor"], self.user) self.assertEqual(called_kwargs["portfolio"], self.portfolio) - # Assert warning message is called correctly - mock_messages_warning.assert_called_once() - warning_args, _ = mock_messages_warning.call_args - self.assertIsInstance(warning_args[0], WSGIRequest) - self.assertEqual(warning_args[1], "Could not send email notification to existing organization admins.") + # Get the arguments passed to send_portfolio_member_permission_update_email + _, called_kwargs = mock_send_update_email.call_args + + # Assert the email content + self.assertEqual(called_kwargs["requestor"], self.user) + self.assertEqual(called_kwargs["permissions"], admin_permission) + + # Assert that messages.warning is called twice + self.assertEqual(mock_messages_warning.call_count, 2) + + # Extract the actual messages sent + warning_messages = [call_args[0][1] for call_args in mock_messages_warning.call_args_list] + + # Check for the expected messages + self.assertIn("Could not send email notification to existing organization admins.", warning_messages) + self.assertIn(f"Could not send email notification to {admin_member.email}.", warning_messages) @less_console_noise_decorator @override_flag("organization_feature", active=True) From 67ab988d1ee1ce8c4067cadda6d5c83f3c8fb90d Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 5 Feb 2025 22:50:01 -0700 Subject: [PATCH 05/50] removed role column --- .../admin/includes/portfolio/portfolio_members_table.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/registrar/templates/django/admin/includes/portfolio/portfolio_members_table.html b/src/registrar/templates/django/admin/includes/portfolio/portfolio_members_table.html index d07e5abf4..6624be95d 100644 --- a/src/registrar/templates/django/admin/includes/portfolio/portfolio_members_table.html +++ b/src/registrar/templates/django/admin/includes/portfolio/portfolio_members_table.html @@ -10,7 +10,6 @@ Title Email Phone - Roles Action @@ -28,11 +27,6 @@ {% endif %} {{ member.user.phone }} - - {% for role in member.user|portfolio_role_summary:original %} - {{ role }} - {% endfor %} - {% if member.user.email %} From 456947e53f1454e7eb5fb145cd95d114d665f789 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:06:05 -0700 Subject: [PATCH 06/50] Add table to each --- src/registrar/templates/admin/analytics.html | 63 ++++++++++++++++++- .../admin/analytics_graph_table.html | 62 ++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 src/registrar/templates/admin/analytics_graph_table.html diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 7c1a09c78..528a20297 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -120,7 +120,7 @@ -
+
+ +
+
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=None %} +
+
+
+
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=None %} +
+
+
+
-
+
-
+
+
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=None %} +
+
+
+
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=None %} +
+
+
+
+ +
+
+
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=None %} +
+
+
+
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=None %} +
+
+
+
+
diff --git a/src/registrar/templates/admin/analytics_graph_table.html b/src/registrar/templates/admin/analytics_graph_table.html new file mode 100644 index 000000000..c339c63c2 --- /dev/null +++ b/src/registrar/templates/admin/analytics_graph_table.html @@ -0,0 +1,62 @@ +{{ data }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeFor start datefor end date
Totaldatadata
Federaldatadata
Interstatedatadata
State/Territorydatadata
Tribaldatadata
Countydatadata
Citydatadata
Special Districtdatadata
School Districtdatadata
Election Boarddatadata
\ No newline at end of file From 499c8082b3086f882fe1e3629b4c0d5e40178c62 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:06:38 -0700 Subject: [PATCH 07/50] Add tables --- src/registrar/templates/admin/analytics.html | 212 +++++++++--------- .../admin/analytics_graph_table.html | 70 ++---- src/registrar/views/report_views.py | 70 ++++-- 3 files changed, 170 insertions(+), 182 deletions(-) diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 528a20297..014f948d1 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -122,136 +122,134 @@
- -

Chart: Managed domains

-

{{ data.managed_domains_sliced_at_end_date.0 }} managed domains for {{ data.end_date }}

-
+ +

Chart: Managed domains

+

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

+
- -

Chart: Unmanaged domains

-

{{ data.unmanaged_domains_sliced_at_end_date.0 }} unmanaged domains for {{ data.end_date }}

-
+ +

Chart: Unmanaged domains

+

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

+
-
- -
+
+ +
-
- Details for managed domains -
- {% include "admin/analytics_graph_table.html" with data=None %} -
-
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} +
+
-
- Details for unmanaged domains -
- {% include "admin/analytics_graph_table.html" with data=None %} -
-
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} +
+
-
- -
+
+ +
- -

Chart: Deleted domains

-

{{ data.deleted_domains_sliced_at_end_date.0 }} deleted domains for {{ data.end_date }}

-
+ +

Chart: Deleted domains

+

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

+
- -

Chart: Ready domains

-

{{ data.ready_domains_sliced_at_end_date.0 }} ready domains for {{ data.end_date }}

-
+ +

Chart: Ready domains

+

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

+
-
- -
+
+ +
-
- Details for deleted domains -
- {% include "admin/analytics_graph_table.html" with data=None %} -
-
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} +
+
-
- Details for ready domains -
- {% include "admin/analytics_graph_table.html" with data=None %} -
-
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} +
+
-
- -
+
+ +
- -

Chart: Submitted requests

-

{{ data.submitted_requests_sliced_at_end_date.0 }} submitted requests for {{ data.end_date }}

-
+ +

Chart: Submitted requests

+

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

+
- -

Chart: All requests

-

{{ data.requests_sliced_at_end_date.0 }} requests for {{ data.end_date }}

-
+ +

Chart: All requests

+

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

+
-
- -
+
+ +
-
- Details for submitted requests -
- {% include "admin/analytics_graph_table.html" with data=None %} -
-
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} +
+
-
- Details for all requests -
- {% include "admin/analytics_graph_table.html" with data=None %} -
-
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} +
+
-
-
diff --git a/src/registrar/templates/admin/analytics_graph_table.html b/src/registrar/templates/admin/analytics_graph_table.html index c339c63c2..58132d023 100644 --- a/src/registrar/templates/admin/analytics_graph_table.html +++ b/src/registrar/templates/admin/analytics_graph_table.html @@ -1,62 +1,26 @@ -{{ data }} - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {% comment %} + This ugly notation is equivalent to data.property_name.start_date_count.index. + Or represented in the pure python way: data[property_name]["start_date_count"][index] + {% endcomment %} + {% with start_counts=data|get_item:property_name|get_item:"start_date_count" end_counts=data|get_item:property_name|get_item:"end_date_count" %} + {% for org_count_type in data.org_count_types %} + {% with index=forloop.counter %} + + + + + + {% endwith %} + {% endfor %} + {% endwith %}
TypeFor start datefor end dateStart date {{ data.start_date }}End date {{ data.end_date }}
Totaldatadata
Federaldatadata
Interstatedatadata
State/Territorydatadata
Tribaldatadata
Countydatadata
Citydatadata
Special Districtdatadata
School Districtdatadata
Election Boarddatadata
{{ org_count_type }}{{ start_counts|slice:index|last }}{{ end_counts|slice:index|last }}
\ No newline at end of file diff --git a/src/registrar/views/report_views.py b/src/registrar/views/report_views.py index 1b9198c79..596e69a5c 100644 --- a/src/registrar/views/report_views.py +++ b/src/registrar/views/report_views.py @@ -126,28 +126,54 @@ class AnalyticsView(View): # include it in the larger context dictionary so it's available in the template rendering context. # This ensures that the admin interface styling and behavior are consistent with other admin pages. **admin.site.each_context(request), - data=dict( - user_count=models.User.objects.all().count(), - domain_count=models.Domain.objects.all().count(), - ready_domain_count=models.Domain.objects.filter(state=models.Domain.State.READY).count(), - last_30_days_applications=last_30_days_applications.count(), - last_30_days_approved_applications=last_30_days_approved_applications.count(), - average_application_approval_time_last_30_days=avg_approval_time_display, - managed_domains_sliced_at_start_date=managed_domains_sliced_at_start_date, - unmanaged_domains_sliced_at_start_date=unmanaged_domains_sliced_at_start_date, - managed_domains_sliced_at_end_date=managed_domains_sliced_at_end_date, - unmanaged_domains_sliced_at_end_date=unmanaged_domains_sliced_at_end_date, - ready_domains_sliced_at_start_date=ready_domains_sliced_at_start_date, - deleted_domains_sliced_at_start_date=deleted_domains_sliced_at_start_date, - ready_domains_sliced_at_end_date=ready_domains_sliced_at_end_date, - deleted_domains_sliced_at_end_date=deleted_domains_sliced_at_end_date, - requests_sliced_at_start_date=requests_sliced_at_start_date, - submitted_requests_sliced_at_start_date=submitted_requests_sliced_at_start_date, - requests_sliced_at_end_date=requests_sliced_at_end_date, - submitted_requests_sliced_at_end_date=submitted_requests_sliced_at_end_date, - start_date=start_date, - end_date=end_date, - ), + data={ + # Tracks what kind of orgs we are keeping count of. + # Used for the details table beneath the graph. + "org_count_types": [ + "Total", + "Federal", + "Interstate", + "State/Territory", + "Tribal", + "County", + "City", + "Special District", + "School District", + "Election Board", + ], + "user_count": models.User.objects.all().count(), + "domain_count": models.Domain.objects.all().count(), + "ready_domain_count": models.Domain.objects.filter(state=models.Domain.State.READY).count(), + "last_30_days_applications": last_30_days_applications.count(), + "last_30_days_approved_applications": last_30_days_approved_applications.count(), + "average_application_approval_time_last_30_days": avg_approval_time_display, + "managed_domains": { + "start_date_count": managed_domains_sliced_at_start_date, + "end_date_count": managed_domains_sliced_at_end_date, + }, + "unmanaged_domains": { + "start_date_count": unmanaged_domains_sliced_at_start_date, + "end_date_count": unmanaged_domains_sliced_at_end_date, + }, + "ready_domains": { + "start_date_count": ready_domains_sliced_at_start_date, + "end_date_count": ready_domains_sliced_at_end_date, + }, + "deleted_domains": { + "start_date_count": deleted_domains_sliced_at_start_date, + "end_date_count": deleted_domains_sliced_at_end_date, + }, + "requests": { + "start_date_count": requests_sliced_at_start_date, + "end_date_count": requests_sliced_at_end_date, + }, + "submitted_requests": { + "start_date_count": submitted_requests_sliced_at_start_date, + "end_date_count": submitted_requests_sliced_at_end_date, + }, + "start_date": start_date, + "end_date": end_date, + }, ) return render(request, "admin/analytics.html", context) From f539ede376626cb475aff9a6f42a48988436b16a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:53:27 -0700 Subject: [PATCH 08/50] fix style and andi stuff --- src/registrar/assets/src/sass/_theme/_admin.scss | 8 +++++++- src/registrar/templates/admin/analytics_graph_table.html | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 4f75fd2fb..035768dad 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -536,9 +536,15 @@ details.dja-detail-table { background-color: transparent; } + thead tr { + background-color: var(--darkened-bg); + } + td, th { padding-left: 12px; - border: none + border: none; + background-color: var(--darkened-bg); + color: var(--body-quiet-color); } thead > tr > th { diff --git a/src/registrar/templates/admin/analytics_graph_table.html b/src/registrar/templates/admin/analytics_graph_table.html index 58132d023..88b538745 100644 --- a/src/registrar/templates/admin/analytics_graph_table.html +++ b/src/registrar/templates/admin/analytics_graph_table.html @@ -1,9 +1,9 @@ - +
- - - + + + From f154e9b870335fd6f8c31c755ad2e3f01b92cd1e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:37:23 -0700 Subject: [PATCH 09/50] Add desktop / mobile view --- .../assets/src/sass/_theme/_admin.scss | 30 +- src/registrar/templates/admin/analytics.html | 529 +++++++++--------- 2 files changed, 303 insertions(+), 256 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 035768dad..05df9a3c2 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -548,7 +548,6 @@ details.dja-detail-table { } thead > tr > th { - border-radius: 4px; border-top: none; border-bottom: none; } @@ -930,3 +929,32 @@ ul.add-list-reset { background-color: transparent !important; } } + +@media (min-width: 1024px) { + .analytics-dashboard-charts { + // Desktop layout - charts in top row, details in bottom row + display: grid; + gap: 2rem; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "chart1 chart2" + "details1 details2" + "chart3 chart4" + "details3 details4" + "chart5 chart6" + "details5 details6"; + + .chart-1 { grid-area: chart1; } + .details-1 { grid-area: details1; } + .chart-2 { grid-area: chart2; } + .details-2 { grid-area: details2; } + .chart-3 { grid-area: chart3; } + .details-3 { grid-area: details3; } + .chart-4 { grid-area: chart4; } + .details-4 { grid-area: details4; } + .chart-5 { grid-area: chart5; } + .details-5 { grid-area: details5; } + .chart-6 { grid-area: chart6; } + .details-6 { grid-area: details6; } + } +} diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 014f948d1..297f27d46 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -1,258 +1,277 @@ {% extends "admin/base_site.html" %} {% load static %} - -{% block content_title %}

Registrar Analytics

{% endblock %} - -{% block content %} - -
- -
-
-
-

At a glance

-
-
    -
  • User Count: {{ data.user_count }}
  • -
  • Domain Count: {{ data.domain_count }}
  • -
  • Domains in READY state: {{ data.ready_domain_count }}
  • -
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • -
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • -
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • -
-
-
-
- -
- -
-
-
-

Growth reports

-
- {% comment %} - Inputs of type date suck for accessibility. - We'll need to replace those guys with a django form once we figure out how to hook one onto this page. - See the commit "Review for ticket #999" - {% endcomment %} -
-
- - -
-
- - -
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
- -
-
- -

Chart: Managed domains

-

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
-
-
- -

Chart: Unmanaged domains

-

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
-
-
- -
-
-
- Details for managed domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} -
-
-
-
-
- Details for unmanaged domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} -
-
-
-
- -
-
- -

Chart: Deleted domains

-

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
-
-
- -

Chart: Ready domains

-

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
-
-
- -
-
-
- Details for deleted domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} -
-
-
-
-
- Details for ready domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} -
-
-
-
- -
-
- -

Chart: Submitted requests

-

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
-
-
- -

Chart: All requests

-

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
-
-
- -
-
-
- Details for submitted requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} -
-
-
-
-
- Details for all requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} -
-
-
-
-
-
-
-
+{% block content_title %} +

Registrar Analytics

{% endblock %} +{% block content %} +
+
+
+
+

At a glance

+
+
    +
  • User Count: {{ data.user_count }}
  • +
  • Domain Count: {{ data.domain_count }}
  • +
  • Domains in READY state: {{ data.ready_domain_count }}
  • +
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • +
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • +
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • +
+
+
+
+ +
+
+
+
+

Growth reports

+
+ {% comment %} + Inputs of type date suck for accessibility. + We'll need to replace those guys with a django form once we figure out how to hook one onto this page. + See the commit "Review for ticket #999" + {% endcomment %} +
+
+ + +
+
+ + +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ {% comment %} Managed/Unmanaged domains {% endcomment %} +
+
+ +

Chart: Managed domains

+

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} +
+
+
+
+
+
+ +

Chart: Unmanaged domains

+

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} +
+
+
+
+ + {% comment %} Deleted/Ready domains {% endcomment %} +
+
+ +

Chart: Deleted domains

+

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} +
+
+
+
+
+
+ +

Chart: Ready domains

+

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

+
+
+
+
+
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} +
+
+
+
+ + {% comment %} Requests {% endcomment %} +
+
+ +

Chart: Submitted requests

+

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

+
+
+
+
+
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} +
+
+
+
+
+
+ +

Chart: All requests

+

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

+
+
+
+
+
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} +
+
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file From d684fea8edbe091367b992d5bfad85630622df5c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:16:42 -0700 Subject: [PATCH 10/50] test changes --- src/registrar/assets/js/get-gov-reports.js | 35 +++++++++++++++--- .../assets/src/sass/_theme/_admin.scss | 15 ++++---- src/registrar/templates/admin/analytics.html | 36 +++++++++---------- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 8bfe32fdd..650a2c6c7 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -59,6 +59,8 @@ /** An IIFE to initialize the analytics page */ (function () { + // Store chart instances globally within this IIFE + const chartInstances = new Map(); function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { var canvas = document.getElementById(canvasId); if (!canvas) { @@ -80,17 +82,16 @@ borderWidth: 1, data: listOne, backgroundColor: [ - pattern.draw('zigzag-vertical', '#1f77b4'), + pattern.draw("zigzag-vertical", "#1f77b4"), ] }, { label: labelTwo, - backgroundColor: "rgba(75, 192, 192, 0.2)", borderColor: "rgba(75, 192, 192, 1)", borderWidth: 1, data: listTwo, backgroundColor: [ - pattern.draw('diagonal', '#1f77b4'), + pattern.draw("diagonal", "#1f77b4"), ] }, ], @@ -98,7 +99,6 @@ var options = { responsive: true, - maintainAspectRatio: false, plugins: { legend: { position: 'top', @@ -115,11 +115,34 @@ }, }; - new Chart(ctx, { + // Destroy existing chart instance if it exists + if (chartInstances.has(canvasId)) { + chartInstances.get(canvasId).destroy(); + } + + // Create and store new chart instance + const chart = new Chart(ctx, { type: "bar", data: data, options: options, }); + + chartInstances.set(canvasId, chart); + } + + function handleResize() { + // Debounce the resize handler + if (handleResize.timeout) { + clearTimeout(handleResize.timeout); + } + + handleResize.timeout = setTimeout(() => { + chartInstances.forEach((chart, canvasId) => { + if (chart && chart.canvas) { + chart.resize(); + } + }); + }, 100); } function initComparativeColumnCharts() { @@ -130,6 +153,8 @@ createComparativeColumnChart("myChart4", "Ready domains", "Start Date", "End Date"); createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date"); createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date"); + + //window.addEventListener("resize", handleResize); }); }; diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 05df9a3c2..ba7d447ee 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -930,11 +930,11 @@ ul.add-list-reset { } } -@media (min-width: 1024px) { +// Break at tablet view +@media (min-width: 768px) { .analytics-dashboard-charts { // Desktop layout - charts in top row, details in bottom row display: grid; - gap: 2rem; grid-template-columns: 1fr 1fr; grid-template-areas: "chart1 chart2" @@ -945,16 +945,17 @@ ul.add-list-reset { "details5 details6"; .chart-1 { grid-area: chart1; } - .details-1 { grid-area: details1; } .chart-2 { grid-area: chart2; } - .details-2 { grid-area: details2; } .chart-3 { grid-area: chart3; } - .details-3 { grid-area: details3; } .chart-4 { grid-area: chart4; } - .details-4 { grid-area: details4; } .chart-5 { grid-area: chart5; } - .details-5 { grid-area: details5; } .chart-6 { grid-area: chart6; } + .details-1 { grid-area: details1; } + .details-2 { grid-area: details2; } + .details-3 { grid-area: details3; } + .details-4 { grid-area: details4; } + .details-5 { grid-area: details5; } .details-6 { grid-area: details6; } } + } diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 297f27d46..855f7f870 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -140,10 +140,10 @@
-
+
-
- Details for managed domains +
+ Details for managed domains
{% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %}
@@ -163,10 +163,10 @@
-
+
-
- Details for unmanaged domains +
+ Details for unmanaged domains
{% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %}
@@ -188,10 +188,10 @@
-
+
-
- Details for deleted domains +
+ Details for deleted domains
{% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %}
@@ -211,10 +211,10 @@
-
+
-
- Details for ready domains +
+ Details for ready domains
{% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %}
@@ -236,10 +236,10 @@
-
+
-
- Details for submitted requests +
+ Details for submitted requests
{% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %}
@@ -259,10 +259,10 @@
-
+
-
- Details for all requests +
+ Details for all requests
{% include "admin/analytics_graph_table.html" with data=data property_name="requests" %}
From e90e31acb713017ee587c1622b7f4425b6cacce9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:52:09 -0700 Subject: [PATCH 11/50] Closing in --- src/registrar/assets/js/get-gov-reports.js | 2 +- src/registrar/templates/admin/analytics.html | 226 +++++++++---------- 2 files changed, 102 insertions(+), 126 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 650a2c6c7..7af40be45 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -154,7 +154,7 @@ createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date"); createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date"); - //window.addEventListener("resize", handleResize); + window.addEventListener("resize", handleResize); }); }; diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 855f7f870..4628ecfb4 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -127,147 +127,123 @@
{% comment %} Managed/Unmanaged domains {% endcomment %} -
-
- -

Chart: Managed domains

-

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
-
+
+ +

Chart: Managed domains

+

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

+
-
-
-
- Details for managed domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} -
-
-
+
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} +
+
-
- -

Chart: Unmanaged domains

-

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
-
+ +

Chart: Unmanaged domains

+

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

+
-
-
-
- Details for unmanaged domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} -
-
-
+
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} +
+
{% comment %} Deleted/Ready domains {% endcomment %} -
-
- -

Chart: Deleted domains

-

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
-
+
+ +

Chart: Deleted domains

+

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

+
-
-
-
- Details for deleted domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} -
-
-
+
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} +
+
-
-
- -

Chart: Ready domains

-

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
-
+
+ +

Chart: Ready domains

+

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

+
-
-
-
- Details for ready domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} -
-
-
+
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} +
+
{% comment %} Requests {% endcomment %} -
-
- -

Chart: Submitted requests

-

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
-
+
+ +

Chart: Submitted requests

+

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

+
-
-
-
- Details for submitted requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} -
-
-
+
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} +
+
-
-
- -

Chart: All requests

-

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
-
+
+ +

Chart: All requests

+

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

+
-
-
-
- Details for all requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} -
-
-
+
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} +
+
From e45f9f9525a92d34199916e250fd40cc9ef91a67 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:40:43 -0700 Subject: [PATCH 12/50] Fix uneven scaling --- src/registrar/assets/src/sass/_theme/_admin.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index ba7d447ee..c06c9c926 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -930,12 +930,12 @@ ul.add-list-reset { } } -// Break at tablet view -@media (min-width: 768px) { +@media (min-width: 1080px) { .analytics-dashboard-charts { // Desktop layout - charts in top row, details in bottom row display: grid; - grid-template-columns: 1fr 1fr; + // Equal columns each gets 1/2 of the space + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "chart1 chart2" "details1 details2" From 6d180eae71cf9a4a73c96b08ea69e5496a6ee3cf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:08:58 -0700 Subject: [PATCH 13/50] Update analytics.html --- src/registrar/templates/admin/analytics.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 4628ecfb4..b36511206 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -125,7 +125,7 @@ -
+
{% comment %} Managed/Unmanaged domains {% endcomment %}
Date: Mon, 10 Feb 2025 13:21:47 -0700 Subject: [PATCH 14/50] cleanup --- .../assets/src/sass/_theme/_admin.scss | 1 + src/registrar/templates/admin/analytics.html | 24 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index c06c9c926..07749d2dd 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -934,6 +934,7 @@ ul.add-list-reset { .analytics-dashboard-charts { // Desktop layout - charts in top row, details in bottom row display: grid; + gap: 2rem; // Equal columns each gets 1/2 of the space grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index b36511206..3747dc7b6 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -127,7 +127,7 @@
{% comment %} Managed/Unmanaged domains {% endcomment %} -
+
{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
+
Details for managed domains
@@ -146,7 +146,7 @@
-
+
{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
+
Details for unmanaged domains
@@ -167,7 +167,7 @@
{% comment %} Deleted/Ready domains {% endcomment %} -
+
{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
+
Details for deleted domains
@@ -186,7 +186,7 @@
-
+
{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
+
Details for ready domains
@@ -207,7 +207,7 @@
{% comment %} Requests {% endcomment %} -
+
{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
+
Details for submitted requests
@@ -226,7 +226,7 @@
-
+
{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
+
Details for all requests
From e900255e681b35b3208bc85457d97bcbb4df25f9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:06:10 -0700 Subject: [PATCH 15/50] fix cutoff bug --- src/registrar/assets/js/get-gov-reports.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 428b576ee..2cc8157b5 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -100,6 +100,7 @@ var options = { responsive: true, + maintainAspectRatio: false, plugins: { legend: { position: 'top', From 12fa9548a136521c910926994ee7e18cc53bd750 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:07:41 -0700 Subject: [PATCH 16/50] Update get-gov-reports.js --- src/registrar/assets/js/get-gov-reports.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js index 2cc8157b5..d458ae05a 100644 --- a/src/registrar/assets/js/get-gov-reports.js +++ b/src/registrar/assets/js/get-gov-reports.js @@ -59,7 +59,6 @@ /** An IIFE to initialize the analytics page */ (function () { - // Store chart instances globally within this IIFE const chartInstances = new Map(); function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { var canvas = document.getElementById(canvasId); @@ -117,12 +116,10 @@ }, }; - // Destroy existing chart instance if it exists if (chartInstances.has(canvasId)) { chartInstances.get(canvasId).destroy(); } - // Create and store new chart instance const chart = new Chart(ctx, { type: "bar", data: data, From 4e95c77725a716d375cc6d097a56e35c4993ed8a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:54:12 -0700 Subject: [PATCH 17/50] cleanup html --- src/registrar/templates/admin/analytics.html | 479 +++++++++---------- 1 file changed, 239 insertions(+), 240 deletions(-) diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index 91248f502..ca3501eec 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -1,6 +1,7 @@ {% extends "admin/base_site.html" %} {% load static %} {% load i18n %} + {% block content_title %}

Registrar Analytics

{% endblock %} {% block breadcrumbs %} @@ -14,251 +15,249 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ {% trans "Analytics Dashboard" %}
{% endblock %} + {% block content %} -
-
-
-
-

At a glance

-
-
    -
  • User Count: {{ data.user_count }}
  • -
  • Domain Count: {{ data.domain_count }}
  • -
  • Domains in READY state: {{ data.ready_domain_count }}
  • -
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • -
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • -
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • -
-
-
-
- -
-
-
-
-

Growth reports

-
- {% comment %} - Inputs of type date suck for accessibility. - We'll need to replace those guys with a django form once we figure out how to hook one onto this page. - See the commit "Review for ticket #999" - {% endcomment %} -
-
- - -
-
- - -
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- {% comment %} Managed/Unmanaged domains {% endcomment %} -
- -

Chart: Managed domains

-

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

-
-
-
-
- Details for managed domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} -
-
-
-
- -

Chart: Unmanaged domains

-

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

-
-
-
-
- Details for unmanaged domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} -
-
-
- {% comment %} Deleted/Ready domains {% endcomment %} -
- -

Chart: Deleted domains

-

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

-
-
-
-
- Details for deleted domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} -
-
-
-
- -

Chart: Ready domains

-

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

-
-
-
-
- Details for ready domains -
- {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} -
-
-
+
- {% comment %} Requests {% endcomment %} -
- -

Chart: Submitted requests

-

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

-
-
-
-
- Details for submitted requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} -
-
-
-
- -

Chart: All requests

-

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

-
-
-
-
- Details for all requests -
- {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} -
-
+
+
+
+

At a glance

+
+
    +
  • User Count: {{ data.user_count }}
  • +
  • Domain Count: {{ data.domain_count }}
  • +
  • Domains in READY state: {{ data.ready_domain_count }}
  • +
  • Domain applications (last 30 days): {{ data.last_30_days_applications }}
  • +
  • Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}
  • +
  • Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}
  • +
+
+ +
+ +
+
+
+

Growth reports

+
+ {% comment %} + Inputs of type date suck for accessibility. + We'll need to replace those guys with a django form once we figure out how to hook one onto this page. + See the commit "Review for ticket #999" + {% endcomment %} +
+
+ + +
+
+ + +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ +
+ {% comment %} Managed/Unmanaged domains {% endcomment %} +
+ +

Chart: Managed domains

+

{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}

+
+
+
+
+ Details for managed domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %} +
+
+
+
+ +

Chart: Unmanaged domains

+

{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}

+
+
+
+
+ Details for unmanaged domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %} +
+
+
+ + {% comment %} Deleted/Ready domains {% endcomment %} +
+ +

Chart: Deleted domains

+

{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}

+
+
+
+
+ Details for deleted domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %} +
+
+
+
+ +

Chart: Ready domains

+

{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}

+
+
+
+
+ Details for ready domains +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %} +
+
+
+ + {% comment %} Requests {% endcomment %} +
+ +

Chart: Submitted requests

+

{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}

+
+
+
+
+ Details for submitted requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %} +
+
+
+
+ +

Chart: All requests

+

{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}

+
+
+
+
+ Details for all requests +
+ {% include "admin/analytics_graph_table.html" with data=data property_name="requests" %} +
+
-
+ +
+
+
+
-{% endblock %} \ No newline at end of file +{% endblock %} From aeb22e99a01d87e0f2a1af61fb70b2b935e2a705 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:53:09 -0700 Subject: [PATCH 18/50] Update package-lock.json --- src/package-lock.json | 1028 +++++++++++++++++++++++------------------ 1 file changed, 566 insertions(+), 462 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 5caff976c..eb2d6b7af 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -53,9 +53,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, "license": "MIT", "engines": { @@ -63,22 +63,23 @@ } }, "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz", + "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/traverse": "^7.26.8", + "@babel/types": "^7.26.8", + "@types/gensync": "^1.0.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -94,14 +95,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz", + "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -123,28 +124,14 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", - "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -177,14 +164,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -271,9 +258,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", "engines": { @@ -299,15 +286,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -316,20 +303,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", @@ -390,27 +363,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", + "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -582,15 +555,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", - "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.8" }, "engines": { "node": ">=6.9.0" @@ -618,13 +591,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -804,13 +777,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", - "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { @@ -953,15 +925,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1040,13 +1011,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1306,13 +1277,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", + "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1322,13 +1293,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1405,15 +1376,15 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", - "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.8.tgz", + "integrity": "sha512-um7Sy+2THd697S4zJEfv/U5MHGJzkN2xhtsR3T/SWRbVSic62nbISh51VVfU9JiO/L/Z97QczHTaFVkOU8IzNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", @@ -1425,9 +1396,9 @@ "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", "@babel/plugin-transform-block-scoping": "^7.25.9", "@babel/plugin-transform-class-properties": "^7.25.9", "@babel/plugin-transform-class-static-block": "^7.26.0", @@ -1438,7 +1409,7 @@ "@babel/plugin-transform-duplicate-keys": "^7.25.9", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-for-of": "^7.25.9", "@babel/plugin-transform-function-name": "^7.25.9", @@ -1447,12 +1418,12 @@ "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", "@babel/plugin-transform-member-expression-literals": "^7.25.9", "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", "@babel/plugin-transform-modules-systemjs": "^7.25.9", "@babel/plugin-transform-modules-umd": "^7.25.9", "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", "@babel/plugin-transform-numeric-separator": "^7.25.9", "@babel/plugin-transform-object-rest-spread": "^7.25.9", "@babel/plugin-transform-object-super": "^7.25.9", @@ -1468,17 +1439,17 @@ "@babel/plugin-transform-shorthand-properties": "^7.25.9", "@babel/plugin-transform-spread": "^7.25.9", "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", "@babel/plugin-transform-unicode-escapes": "^7.25.9", "@babel/plugin-transform-unicode-property-regex": "^7.25.9", "@babel/plugin-transform-unicode-regex": "^7.25.9", "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { @@ -1504,9 +1475,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1517,32 +1488,32 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", + "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz", + "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.8", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/types": "^7.26.8", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1551,9 +1522,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", + "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", "dev": true, "license": "MIT", "dependencies": { @@ -1568,13 +1539,15 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz", "integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==", - "dev": true + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@gulpjs/messages": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -1584,6 +1557,7 @@ "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", "dev": true, + "license": "MIT", "dependencies": { "is-negated-glob": "^1.0.0" }, @@ -1592,9 +1566,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "license": "MIT", "dependencies": { @@ -1694,9 +1668,9 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1714,25 +1688,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -1750,9 +1724,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -1770,9 +1744,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -1790,9 +1764,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -1810,9 +1784,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], @@ -1830,9 +1804,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -1850,9 +1824,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -1870,9 +1844,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], @@ -1890,9 +1864,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -1910,9 +1884,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -1930,9 +1904,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -1950,9 +1924,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -1970,9 +1944,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], @@ -2025,6 +1999,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/gensync": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz", + "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2033,9 +2014,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", + "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2068,6 +2049,7 @@ "resolved": "https://registry.npmjs.org/@uswds/compile/-/compile-1.2.1.tgz", "integrity": "sha512-ODMGF97l8x+eJYp/7U1cB0CnalC5nb+1xEkP0sasG2bJyNqX9U+r7te0YNEURleIfrBOyxGVHVBBAw0gqS0htQ==", "dev": true, + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "autoprefixer": "10.4.20", "del": "6.1.1", @@ -2272,6 +2254,19 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2387,6 +2382,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -2412,6 +2408,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2445,6 +2442,7 @@ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2454,6 +2452,7 @@ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2501,6 +2500,7 @@ "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.4.4", "once": "^1.4.0", @@ -2515,6 +2515,7 @@ "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, + "license": "MIT", "dependencies": { "async-done": "^2.0.0" }, @@ -2541,6 +2542,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -2572,7 +2574,8 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/babel-loader": { "version": "9.2.1", @@ -2608,14 +2611,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2639,6 +2642,7 @@ "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, + "license": "MIT", "dependencies": { "async-done": "^2.0.0", "async-settle": "^2.0.0", @@ -2655,10 +2659,11 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", - "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", "dev": true, + "license": "Apache-2.0", "optional": true }, "node_modules/base64-js": { @@ -2701,6 +2706,7 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2773,6 +2779,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "devOptional": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -2781,9 +2788,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -2801,9 +2808,9 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -2861,9 +2868,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001685", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001685.tgz", - "integrity": "sha512-e/kJN1EMyHQzgcMEEgoo+YTCO1NGCmIYHk5Qk8jT6AazWemS5QFKJ5ShCJlH3GZrNIdZofcNCEwZqbMjjKzmnA==", + "version": "1.0.30001699", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", + "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", "dev": true, "funding": [ { @@ -2886,6 +2893,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2897,18 +2905,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -2962,6 +2958,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3018,6 +3015,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -3068,6 +3066,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -3079,7 +3078,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/color-support": { "version": "1.1.3", @@ -3095,7 +3095,8 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "6.2.1", @@ -3131,6 +3132,7 @@ "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", "dev": true, + "license": "MIT", "dependencies": { "each-props": "^3.0.0", "is-plain-object": "^5.0.0" @@ -3140,13 +3142,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.2" + "browserslist": "^4.24.3" }, "funding": { "type": "opencollective", @@ -3218,9 +3220,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3262,6 +3264,7 @@ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3340,9 +3343,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -3358,6 +3361,7 @@ "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0" @@ -3367,9 +3371,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.68", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz", - "integrity": "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==", + "version": "1.5.97", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz", + "integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==", "dev": true, "license": "ISC" }, @@ -3386,7 +3390,8 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/encoding-sniffer": { "version": "0.2.0", @@ -3411,9 +3416,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -3462,9 +3467,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -3560,6 +3565,7 @@ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, + "license": "MIT", "dependencies": { "homedir-polyfill": "^1.0.1" }, @@ -3571,7 +3577,22 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/extract-zip": { "version": "2.0.1", @@ -3620,12 +3641,13 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3633,7 +3655,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -3651,15 +3673,26 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", "dev": true, + "license": "MIT", "dependencies": { "fastest-levenshtein": "^1.0.7" } }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -3667,14 +3700,15 @@ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.9.1" } }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "dev": true, "license": "ISC", "dependencies": { @@ -3704,6 +3738,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "devOptional": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3750,6 +3785,7 @@ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "dev": true, + "license": "MIT", "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.3", @@ -3765,6 +3801,7 @@ "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, + "license": "MIT", "dependencies": { "expand-tilde": "^2.0.2", "is-plain-object": "^5.0.0", @@ -3781,6 +3818,7 @@ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -3790,6 +3828,7 @@ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3799,6 +3838,7 @@ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", "dev": true, + "license": "MIT", "dependencies": { "for-in": "^1.0.1" }, @@ -3811,6 +3851,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -3830,6 +3871,7 @@ "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.8", "streamx": "^2.12.0" @@ -3850,6 +3892,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3882,6 +3925,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -3940,6 +3984,7 @@ "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", "dev": true, + "license": "MIT", "dependencies": { "@gulpjs/to-absolute-glob": "^4.0.0", "anymatch": "^3.1.3", @@ -3959,6 +4004,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3978,6 +4024,7 @@ "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "dev": true, + "license": "MIT", "dependencies": { "async-done": "^2.0.0", "chokidar": "^3.5.3" @@ -3991,6 +4038,7 @@ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, + "license": "MIT", "dependencies": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", @@ -4005,6 +4053,7 @@ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, + "license": "MIT", "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", @@ -4052,6 +4101,7 @@ "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", "dev": true, + "license": "MIT", "dependencies": { "sparkles": "^2.1.0" }, @@ -4071,6 +4121,7 @@ "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", "dev": true, + "license": "MIT", "dependencies": { "glob-watcher": "^6.0.0", "gulp-cli": "^3.0.0", @@ -4089,6 +4140,7 @@ "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", "dev": true, + "license": "MIT", "dependencies": { "@gulpjs/messages": "^1.1.0", "chalk": "^4.1.2", @@ -4195,6 +4247,7 @@ "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", "dev": true, + "license": "MIT", "dependencies": { "glogg": "^2.2.0" }, @@ -4229,6 +4282,7 @@ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, + "license": "MIT", "dependencies": { "parse-passwd": "^1.0.0" }, @@ -4365,13 +4419,15 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -4390,6 +4446,7 @@ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, + "license": "MIT", "dependencies": { "is-relative": "^1.0.0", "is-windows": "^1.0.1" @@ -4403,6 +4460,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -4411,9 +4469,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -4426,6 +4484,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4441,6 +4525,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4463,6 +4548,7 @@ "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4472,6 +4558,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -4501,6 +4588,7 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4510,6 +4598,7 @@ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, + "license": "MIT", "dependencies": { "is-unc-path": "^1.0.0" }, @@ -4522,6 +4611,7 @@ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, + "license": "MIT", "dependencies": { "unc-path-regex": "^0.1.2" }, @@ -4534,6 +4624,7 @@ "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4543,6 +4634,7 @@ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4558,7 +4650,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", @@ -4602,6 +4695,22 @@ "node": ">= 10.13.0" } }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4610,9 +4719,9 @@ "license": "MIT" }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -4679,6 +4788,7 @@ "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -4688,6 +4798,7 @@ "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -4697,6 +4808,7 @@ "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", "dev": true, + "license": "MIT", "dependencies": { "extend": "^3.0.2", "findup-sync": "^5.0.0", @@ -4756,6 +4868,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "deprecated": "This package is deprecated. Use structuredClone instead.", "dev": true, "license": "MIT" }, @@ -4795,6 +4908,7 @@ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4918,6 +5032,7 @@ "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -4976,9 +5091,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -5000,6 +5115,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5009,6 +5125,7 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5018,6 +5135,7 @@ "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" }, @@ -5051,6 +5169,7 @@ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, + "license": "MIT", "dependencies": { "array-each": "^1.0.1", "array-slice": "^1.0.0", @@ -5066,6 +5185,7 @@ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -5240,6 +5360,7 @@ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, + "license": "MIT", "dependencies": { "is-absolute": "^1.0.0", "map-cache": "^0.2.0", @@ -5264,6 +5385,7 @@ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5336,6 +5458,7 @@ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, + "license": "MIT", "dependencies": { "path-root-regex": "^0.1.0" }, @@ -5348,6 +5471,7 @@ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5450,46 +5574,6 @@ "node": ">= 0.10" } }, - "node_modules/plugin-error/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/plugin-error/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/plugin-error/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -5509,6 +5593,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -5569,7 +5654,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/prepend-http": { "version": "3.0.1", @@ -5761,12 +5847,6 @@ ], "license": "MIT" }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5798,6 +5878,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -5822,6 +5903,7 @@ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve": "^1.20.0" }, @@ -5904,6 +5986,19 @@ "regjsparser": "bin/parser" } }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -5926,6 +6021,7 @@ "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -5947,6 +6043,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5962,19 +6059,22 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5984,6 +6084,7 @@ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, + "license": "MIT", "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" @@ -6003,6 +6104,7 @@ "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", "dev": true, + "license": "MIT", "dependencies": { "value-or-function": "^4.0.0" }, @@ -6084,9 +6186,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", - "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", + "version": "1.84.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz", + "integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -6108,6 +6210,7 @@ "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.0.tgz", "integrity": "sha512-/8cYZeL39evUqe0o//193na51Q1VWZ61qhxioQvLJwOtWIrX+PgNhCyD8RSuTtmzc4+6+waFZf899bfp/MCUwA==", "dev": true, + "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", "buffer-builder": "^0.2.0", @@ -6155,6 +6258,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -6171,6 +6275,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -6187,6 +6292,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -6203,6 +6309,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -6219,6 +6326,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -6235,6 +6343,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -6251,6 +6360,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -6267,6 +6377,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6283,6 +6394,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6299,6 +6411,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6315,6 +6428,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6331,6 +6445,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6347,6 +6462,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6363,6 +6479,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6379,6 +6496,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6395,6 +6513,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6411,6 +6530,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -6427,6 +6547,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -6443,6 +6564,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -6459,6 +6581,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -6467,6 +6590,22 @@ "node": ">=14.0.0" } }, + "node_modules/sass-embedded/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/sass-loader": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", @@ -6507,9 +6646,9 @@ } }, "node_modules/sass/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -6522,12 +6661,12 @@ } }, "node_modules/sass/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -6535,9 +6674,9 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "license": "MIT", "dependencies": { @@ -6547,7 +6686,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -6555,9 +6694,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6571,6 +6710,7 @@ "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, + "license": "MIT", "dependencies": { "sver": "^1.8.3" }, @@ -6599,9 +6739,9 @@ } }, "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6628,11 +6768,22 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sparkles": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -6642,6 +6793,7 @@ "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", "dev": true, + "license": "MIT", "dependencies": { "streamx": "^2.13.2" } @@ -6650,16 +6802,17 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/streamx": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", - "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", "dev": true, + "license": "MIT", "dependencies": { "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", "text-decoder": "^1.1.0" }, "optionalDependencies": { @@ -6680,6 +6833,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6703,19 +6857,16 @@ } }, "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -6736,6 +6887,7 @@ "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, + "license": "MIT", "optionalDependencies": { "semver": "^6.3.0" } @@ -6745,6 +6897,7 @@ "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", "dev": true, + "license": "MIT", "dependencies": { "sync-message-port": "^1.0.0" }, @@ -6757,6 +6910,7 @@ "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", "dev": true, + "license": "MIT", "engines": { "node": ">=16.0.0" } @@ -6772,9 +6926,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -6818,14 +6972,15 @@ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, + "license": "MIT", "dependencies": { "streamx": "^2.12.5" } }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.38.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.2.tgz", + "integrity": "sha512-w8CXxxbFA5zfNsR/i8HZq5bvn18AK0O9jj7hyo1YqkovLxEFa0uP0LCVGZRqiRaKRFxXhELBp8SteeAjEnfeJg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6842,17 +6997,17 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -6876,72 +7031,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -6954,6 +7043,7 @@ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } @@ -6992,6 +7082,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "devOptional": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -7004,6 +7095,7 @@ "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "dev": true, + "license": "MIT", "dependencies": { "streamx": "^2.12.5" }, @@ -7045,6 +7137,7 @@ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7054,6 +7147,7 @@ "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, + "license": "MIT", "dependencies": { "bach": "^2.0.1", "fast-levenshtein": "^3.0.0", @@ -7069,6 +7163,7 @@ "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -7134,9 +7229,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "funding": [ { @@ -7155,7 +7250,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7185,6 +7280,7 @@ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -7194,6 +7290,7 @@ "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } @@ -7228,6 +7325,7 @@ "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^5.0.0", "vinyl": "^3.0.0" @@ -7241,6 +7339,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", @@ -7266,6 +7365,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -7276,6 +7376,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -7290,6 +7391,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, + "license": "MIT", "dependencies": { "clone": "^2.1.2", "clone-stats": "^1.0.0", @@ -7306,6 +7408,7 @@ "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", "dev": true, + "license": "MIT", "dependencies": { "fs-mkdirp-stream": "^2.0.1", "glob-stream": "^8.0.0", @@ -7331,6 +7434,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, + "license": "MIT", "dependencies": { "clone": "^2.1.2", "clone-stats": "^1.0.0", @@ -7347,6 +7451,7 @@ "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, + "license": "MIT", "dependencies": { "convert-source-map": "^2.0.0", "graceful-fs": "^4.2.10", @@ -7364,6 +7469,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, + "license": "MIT", "dependencies": { "clone": "^2.1.2", "clone-stats": "^1.0.0", @@ -7385,16 +7491,6 @@ "source-map": "^0.5.1" } }, - "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/vinyl/node_modules/replace-ext": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", @@ -7426,17 +7522,17 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", @@ -7505,17 +7601,20 @@ "webpack": "^5.21.2" } }, - "node_modules/webpack/node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "node_modules/webpack-stream/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.4.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/webpack/node_modules/ajv": { @@ -7607,6 +7706,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -7625,6 +7725,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -7669,6 +7770,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -7695,6 +7797,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -7723,6 +7826,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } From 07cc070c13f0b681717352286fba397aebc54c44 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:04:40 -0700 Subject: [PATCH 19/50] test 2 --- src/.pa11yci | 28 ++++++++++++++-------------- src/package-lock.json | 2 +- src/package.json | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 571d0b1c8..ed382a38a 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -8,20 +8,20 @@ "http://localhost:8080/health/", "http://localhost:8080/request/", "http://localhost:8080/request/start", - "http://localhost:8080/request/organization/", - "http://localhost:8080/request/org_federal/", - "http://localhost:8080/request/org_election/", - "http://localhost:8080/request/org_contact/", - "http://localhost:8080/request/senior_official/", - "http://localhost:8080/request/current_sites/", - "http://localhost:8080/request/dotgov_domain/", - "http://localhost:8080/request/purpose/", - "http://localhost:8080/request/your_contact/", - "http://localhost:8080/request/other_contacts/", - "http://localhost:8080/request/anything_else/", - "http://localhost:8080/request/requirements/", - "http://localhost:8080/request/finished/", - "http://localhost:8080/request/requesting_entity/", + "http://localhost:8080/request/1/organization/", + "http://localhost:8080/request/1/org_federal/", + "http://localhost:8080/request/1/org_election/", + "http://localhost:8080/request/1/org_contact/", + "http://localhost:8080/request/1/senior_official/", + "http://localhost:8080/request/1/current_sites/", + "http://localhost:8080/request/1/dotgov_domain/", + "http://localhost:8080/request/1/purpose/", + "http://localhost:8080/request/1/your_contact/", + "http://localhost:8080/request/1/other_contacts/", + "http://localhost:8080/request/1/anything_else/", + "http://localhost:8080/request/1/requirements/", + "http://localhost:8080/request/1/finished/", + "http://localhost:8080/request/1/requesting_entity/", "http://localhost:8080/user-profile/", "http://localhost:8080/members/", "http://localhost:8080/members/new-member" diff --git a/src/package-lock.json b/src/package-lock.json index eb2d6b7af..0f2a8c38b 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@uswds/uswds": "3.8.1", - "pa11y-ci": "^3.0.1", + "pa11y-ci": "^3.1.0", "sass": "^1.54.8" }, "devDependencies": { diff --git a/src/package.json b/src/package.json index d915b2384..9c50c93e1 100644 --- a/src/package.json +++ b/src/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "@uswds/uswds": "3.8.1", - "pa11y-ci": "^3.0.1", + "pa11y-ci": "^3.1.0", "sass": "^1.54.8" }, "devDependencies": { From bb05f6f4657c1d6aaab06e3658b128ee86f744a8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 11 Feb 2025 12:50:40 -0800 Subject: [PATCH 20/50] Add logic for getting envs and adding it to email subject and body --- src/registrar/config/settings.py | 1 + .../templates/emails/domain_invitation.txt | 2 +- .../emails/domain_invitation_subject.txt | 2 +- ...n_manager_deleted_notification_subject.txt | 2 +- .../emails/domain_manager_notification.txt | 2 +- .../domain_manager_notification_subject.txt | 2 +- .../emails/domain_request_withdrawn.txt | 2 +- .../domain_request_withdrawn_subject.txt | 2 +- .../templates/emails/metadata_body.txt | 2 +- .../templates/emails/metadata_subject.txt | 2 +- .../portfolio_admin_addition_notification.txt | 2 +- ...io_admin_addition_notification_subject.txt | 2 +- .../portfolio_admin_removal_notification.txt | 2 +- ...lio_admin_removal_notification_subject.txt | 2 +- .../templates/emails/portfolio_invitation.txt | 2 +- .../emails/portfolio_invitation_subject.txt | 2 +- .../emails/status_change_approved.txt | 2 +- .../emails/status_change_approved_subject.txt | 2 +- .../emails/status_change_subject.txt | 2 +- .../emails/submission_confirmation.txt | 2 +- .../submission_confirmation_subject.txt | 2 +- .../emails/transition_domain_invitation.txt | 2 +- .../transition_domain_invitation_subject.txt | 2 +- .../emails/update_to_approved_domain.txt | 4 ++-- .../update_to_approved_domain_subject.txt | 2 +- src/registrar/utility/email.py | 19 +++++++++++++++++++ 26 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 78439188e..fa4c2d8dc 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -107,6 +107,7 @@ DEBUG = env_debug # Controls production specific feature toggles IS_PRODUCTION = env_is_production SECRET_ENCRYPT_METADATA = secret_encrypt_metadata +BASE_URL = env_base_url # Applications are modular pieces of code. # They are provided by Django, by third-parties, or by yourself. diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index a077bff26..270786a7a 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -4,7 +4,7 @@ Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first {{ requestor_email }} has invited you to manage: {% for domain in domains %}{{ domain.name }} {% endfor %} -To manage domain information, visit the .gov registrar . +To manage domain information, visit the .gov registrar <{{ manage_url }}>. ---------------------------------------------------------------- {% if not requested_user %} diff --git a/src/registrar/templates/emails/domain_invitation_subject.txt b/src/registrar/templates/emails/domain_invitation_subject.txt index 9663346d0..9f15c38b4 100644 --- a/src/registrar/templates/emails/domain_invitation_subject.txt +++ b/src/registrar/templates/emails/domain_invitation_subject.txt @@ -1 +1 @@ -You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file +{{ prefix }}You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt index c84a20f18..7376bdb86 100644 --- a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt @@ -1 +1 @@ -A domain manager was removed from {{ domain.name }} \ No newline at end of file +{{ prefix }}A domain manager was removed from {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_notification.txt b/src/registrar/templates/emails/domain_manager_notification.txt index c253937e4..18e682329 100644 --- a/src/registrar/templates/emails/domain_manager_notification.txt +++ b/src/registrar/templates/emails/domain_manager_notification.txt @@ -15,7 +15,7 @@ The person who received the invitation will become a domain manager once they lo associated with the invited email address. If you need to cancel this invitation or remove the domain manager, you can do that by going to -this domain in the .gov registrar . +this domain in the .gov registrar <{{ manage_url }}. WHY DID YOU RECEIVE THIS EMAIL? diff --git a/src/registrar/templates/emails/domain_manager_notification_subject.txt b/src/registrar/templates/emails/domain_manager_notification_subject.txt index 0e9918de0..8560cb9fa 100644 --- a/src/registrar/templates/emails/domain_manager_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_notification_subject.txt @@ -1 +1 @@ -A domain manager was invited to {{ domain.name }} \ No newline at end of file +{{ prefix }}A domain manager was invited to {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index fbdf5b4f1..fe026027b 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -11,7 +11,7 @@ STATUS: Withdrawn ---------------------------------------------------------------- YOU CAN EDIT YOUR WITHDRAWN REQUEST -You can edit and resubmit this request by signing in to the registrar . +You can edit and resubmit this request by signing in to the registrar <{{ manage_url }}>. SOMETHING WRONG? diff --git a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/metadata_body.txt b/src/registrar/templates/emails/metadata_body.txt index adf0a186c..a0a3682b7 100644 --- a/src/registrar/templates/emails/metadata_body.txt +++ b/src/registrar/templates/emails/metadata_body.txt @@ -1 +1 @@ -An export of all .gov metadata. +{{ prefix }}An export of all .gov metadata. diff --git a/src/registrar/templates/emails/metadata_subject.txt b/src/registrar/templates/emails/metadata_subject.txt index 5fdece7ef..c19b4c26e 100644 --- a/src/registrar/templates/emails/metadata_subject.txt +++ b/src/registrar/templates/emails/metadata_subject.txt @@ -1,2 +1,2 @@ -Domain metadata - {{current_date_str}} +{{ prefix }}Domain metadata - {{current_date_str}} diff --git a/src/registrar/templates/emails/portfolio_admin_addition_notification.txt b/src/registrar/templates/emails/portfolio_admin_addition_notification.txt index b8953aa67..9e6da3985 100644 --- a/src/registrar/templates/emails/portfolio_admin_addition_notification.txt +++ b/src/registrar/templates/emails/portfolio_admin_addition_notification.txt @@ -16,7 +16,7 @@ The person who received the invitation will become an admin once they log in to associated with the invited email address. If you need to cancel this invitation or remove the admin, you can do that by going to -the Members section for your organization . +the Members section for your organization <{{ manage_url }}>. WHY DID YOU RECEIVE THIS EMAIL? diff --git a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt index 3d6b2a140..ee5987512 100644 --- a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt @@ -1 +1 @@ -An admin was invited to your .gov organization \ No newline at end of file +{{ prefix }}An admin was invited to your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification.txt index 6a536aa49..bf0338c03 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification.txt @@ -8,7 +8,7 @@ REMOVED BY: {{ requestor_email }} REMOVED ON: {{date}} ADMIN REMOVED: {{ removed_email_address }} -You can view this update by going to the Members section for your .gov organization . +You can view this update by going to the Members section for your .gov organization <{{ manage_url }}>. ---------------------------------------------------------------- diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt index e250b17f8..030d27ae7 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt @@ -1 +1 @@ -An admin was removed from your .gov organization \ No newline at end of file +{{ prefix}}An admin was removed from your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_invitation.txt b/src/registrar/templates/emails/portfolio_invitation.txt index 775b74c7c..893da153d 100644 --- a/src/registrar/templates/emails/portfolio_invitation.txt +++ b/src/registrar/templates/emails/portfolio_invitation.txt @@ -3,7 +3,7 @@ Hi. {{ requestor_email }} has invited you to {{ portfolio.organization_name }}. -You can view this organization on the .gov registrar . +You can view this organization on the .gov registrar <{{ manage_url }}>. ---------------------------------------------------------------- diff --git a/src/registrar/templates/emails/portfolio_invitation_subject.txt b/src/registrar/templates/emails/portfolio_invitation_subject.txt index 552bb2bec..de9080196 100644 --- a/src/registrar/templates/emails/portfolio_invitation_subject.txt +++ b/src/registrar/templates/emails/portfolio_invitation_subject.txt @@ -1 +1 @@ -You’ve been invited to a .gov organization \ No newline at end of file +{{ prefix }}You’ve been invited to a .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 821e89e42..635b36cbd 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -8,7 +8,7 @@ REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Approved -You can manage your approved domain on the .gov registrar . +You can manage your approved domain on the .gov registrar <{{ manage_url }}>. ---------------------------------------------------------------- diff --git a/src/registrar/templates/emails/status_change_approved_subject.txt b/src/registrar/templates/emails/status_change_approved_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/status_change_approved_subject.txt +++ b/src/registrar/templates/emails/status_change_approved_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/status_change_subject.txt b/src/registrar/templates/emails/status_change_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/status_change_subject.txt +++ b/src/registrar/templates/emails/status_change_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index d9d01ec3e..afbde48d5 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -20,7 +20,7 @@ During our review, we’ll verify that: - You work at the organization and/or can make requests on its behalf - Your requested domain meets our naming requirements {% endif %} -We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. . +We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <{{ manage_url }}>. NEED TO MAKE CHANGES? diff --git a/src/registrar/templates/emails/submission_confirmation_subject.txt b/src/registrar/templates/emails/submission_confirmation_subject.txt index 51b2c745a..cc146643a 100644 --- a/src/registrar/templates/emails/submission_confirmation_subject.txt +++ b/src/registrar/templates/emails/submission_confirmation_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt index b6773d9e9..dc812edf3 100644 --- a/src/registrar/templates/emails/transition_domain_invitation.txt +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -31,7 +31,7 @@ CHECK YOUR .GOV DOMAIN CONTACTS This is a good time to check who has access to your .gov domain{% if domains|length > 1 %}s{% endif %}. The admin, technical, and billing contacts listed for your domain{% if domains|length > 1 %}s{% endif %} in our old system also received this email. In our new registrar, these contacts are all considered “domain managers.” We no longer have the admin, technical, and billing roles, and you aren’t limited to three domain managers like in the old system. - 1. Once you have your Login.gov account, sign in to the new registrar at . + 1. Once you have your Login.gov account, sign in to the new registrar at <{{ manage_url }}>. 2. Click the “Manage” link next to your .gov domain, then click on “Domain managers” to see who has access to your domain. 3. If any of these users should not have access to your domain, let us know in a reply to this email. diff --git a/src/registrar/templates/emails/transition_domain_invitation_subject.txt b/src/registrar/templates/emails/transition_domain_invitation_subject.txt index 526c7714b..b162341d9 100644 --- a/src/registrar/templates/emails/transition_domain_invitation_subject.txt +++ b/src/registrar/templates/emails/transition_domain_invitation_subject.txt @@ -1 +1 @@ -(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file +{{ prefix }}(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file diff --git a/src/registrar/templates/emails/update_to_approved_domain.txt b/src/registrar/templates/emails/update_to_approved_domain.txt index 99f86ea54..fb0a442cb 100644 --- a/src/registrar/templates/emails/update_to_approved_domain.txt +++ b/src/registrar/templates/emails/update_to_approved_domain.txt @@ -1,4 +1,4 @@ -{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} + {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi, An update was made to a domain you manage. @@ -8,7 +8,7 @@ UPDATED BY: {{user}} UPDATED ON: {{date}} INFORMATION UPDATED: {{changes}} -You can view this update in the .gov registrar . +You can view this update in the .gov registrar <{{ manage_url }}>. Get help with managing your .gov domain . diff --git a/src/registrar/templates/emails/update_to_approved_domain_subject.txt b/src/registrar/templates/emails/update_to_approved_domain_subject.txt index cf4c9a14c..d952999a0 100644 --- a/src/registrar/templates/emails/update_to_approved_domain_subject.txt +++ b/src/registrar/templates/emails/update_to_approved_domain_subject.txt @@ -1 +1 @@ -An update was made to {{domain}} \ No newline at end of file +{{ prefix }}An update was made to {{domain}} \ No newline at end of file diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 40601cdc7..535096b10 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -3,6 +3,7 @@ import boto3 import logging import textwrap +import re from datetime import datetime from django.apps import apps from django.conf import settings @@ -48,6 +49,24 @@ def send_templated_email( # noqa No valid recipient addresses are provided """ + if context is None: + context = {} + + env_base_url = settings.BASE_URL + # The regular expresstion is to get both http (localhost) and https (everything else) + env_name = re.sub(r"^https?://", "", env_base_url).split(".")[0] + # To add to subject lines ie [GETGOV-RH] + prefix = f"[{env_name.upper()}] " if not settings.IS_PRODUCTION else "" + # For email links + manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" + + # Adding to context + context.update( + { + "prefix": prefix, + "manage_url": manage_url, + } + ) # by default assume we can send to all addresses (prod has no whitelist) sendable_cc_addresses = cc_addresses From 898a66ccc90fcb1058afe5e493da36e43736cfe7 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 12 Feb 2025 09:44:39 -0500 Subject: [PATCH 21/50] removed domain renewal feature --- src/registrar/context_processors.py | 3 -- src/registrar/models/domain.py | 2 +- src/registrar/models/user.py | 3 -- src/registrar/templates/domain_detail.html | 10 ++--- src/registrar/templates/domain_sidebar.html | 2 +- .../templates/includes/domains_table.html | 6 +-- src/registrar/tests/test_views_domain.py | 39 +++---------------- src/registrar/views/domain.py | 3 +- 8 files changed, 16 insertions(+), 52 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index a078c81ac..061c0ab4f 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -68,7 +68,6 @@ def portfolio_permissions(request): "has_organization_requests_flag": False, "has_organization_members_flag": False, "is_portfolio_admin": False, - "has_domain_renewal_flag": False, } try: portfolio = request.session.get("portfolio") @@ -77,7 +76,6 @@ def portfolio_permissions(request): portfolio_context.update( { "has_organization_feature_flag": True, - "has_domain_renewal_flag": request.user.has_domain_renewal_flag(), } ) @@ -95,7 +93,6 @@ def portfolio_permissions(request): "has_organization_requests_flag": request.user.has_organization_requests_flag(), "has_organization_members_flag": request.user.has_organization_members_flag(), "is_portfolio_admin": request.user.is_portfolio_admin(portfolio), - "has_domain_renewal_flag": request.user.has_domain_renewal_flag(), } return portfolio_context diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 0f0b3f112..cd768f76c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1583,7 +1583,7 @@ class Domain(TimeStampedModel, DomainHelper): # Given expired is not a physical state, but it is displayed as such, # We need custom logic to determine this message. help_text = "This domain has expired. Complete the online renewal process to maintain access." - elif flag_is_active(request, "domain_renewal") and self.is_expiring(): + elif self.is_expiring(): help_text = "This domain is expiring soon. Complete the online renewal process to maintain access." else: help_text = Domain.State.get_help_text(self.state) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 6f8ee499b..82a0465c5 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -271,9 +271,6 @@ class User(AbstractUser): def is_portfolio_admin(self, portfolio): return "Admin" in self.portfolio_role_summary(portfolio) - def has_domain_renewal_flag(self): - return flag_is_active_for_user(self, "domain_renewal") - def get_first_portfolio(self): permission = self.portfolio_permissions.first() if permission: diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 758c43366..57749f038 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -35,7 +35,7 @@ {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} Expired - {% elif has_domain_renewal_flag and domain.is_expiring %} + {% elif domain.is_expiring %} Expiring soon {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} DNS needed @@ -46,17 +46,17 @@ {% if domain.get_state_help_text %}

- {% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %} + {% if domain.is_expired and is_domain_manager %} This domain has expired, but it is still online. {% url 'domain-renewal' pk=domain.id as url %} Renew to maintain access. - {% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %} + {% elif domain.is_expiring and is_domain_manager %} This domain will expire soon. {% url 'domain-renewal' pk=domain.id as url %} Renew to maintain access. - {% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %} + {% elif domain.is_expiring and is_portfolio_user %} This domain will expire soon. Contact one of the listed domain managers to renew the domain. - {% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %} + {% elif domain.is_expired and is_portfolio_user %} This domain has expired, but it is still online. Contact one of the listed domain managers to renew the domain. {% else %} {{ domain.get_state_help_text }} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 5946b6859..3302a6a79 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -81,7 +81,7 @@ {% endwith %} - {% if has_domain_renewal_flag and is_domain_manager%} + {% if is_domain_manager%} {% if domain.is_expiring or domain.is_expired %} {% with url_name="domain-renewal" %} {% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %} diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 94cb4ea6d..3cf04a830 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -9,7 +9,7 @@ -{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %} +{% if num_expiring_domains > 0 and has_any_domains_portfolio_permission %}

@@ -75,7 +75,7 @@
- {% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %} + {% if num_expiring_domains > 0 and not portfolio %}
@@ -173,7 +173,6 @@ >Deleted
- {% if has_domain_renewal_flag %}
Expiring soon
- {% endif %}
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index dc5bff27a..2f1bcf5e3 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -477,7 +477,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.domain_with_ip.expiration_date = self.expiration_date_one_year_out() self.domain_with_ip.save() - @override_flag("domain_renewal", active=True) def test_expiring_domain_on_detail_page_as_domain_manager(self): """If a user is a domain manager and their domain is expiring soon, user should be able to see the "Renew to maintain access" link domain overview detail box.""" @@ -496,7 +495,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertNotContains(detail_page, "DNS needed") self.assertNotContains(detail_page, "Expired") - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) def test_expiring_domain_on_detail_page_in_org_model_as_a_non_domain_manager(self): """In org model: If a user is NOT a domain manager and their domain is expiring soon, @@ -534,7 +532,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): ) self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.") - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self): """Inorg model: If a user is a domain manager and their domain is expiring soon, @@ -555,7 +552,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): ) self.assertContains(detail_page, "Renew to maintain access") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_and_sidebar_expiring(self): """If a user is a domain manager and their domain is expiring soon, user should be able to see Renewal Form on the sidebar.""" @@ -584,7 +580,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(response.status_code, 200) self.assertContains(response, f"Renew {self.domain_to_renew.name}") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_and_sidebar_expired(self): """If a user is a domain manager and their domain is expired, user should be able to see Renewal Form on the sidebar.""" @@ -614,7 +609,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(response.status_code, 200) self.assertContains(response, f"Renew {self.domain_to_renew.name}") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_your_contact_info_edit(self): """Checking that if a user is a domain manager they can edit the Your Profile portion of the Renewal Form.""" @@ -634,7 +628,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "Review the details below and update any required information") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_security_email_edit(self): """Checking that if a user is a domain manager they can edit the Security Email portion of the Renewal Form.""" @@ -657,7 +650,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "A security contact should be capable of evaluating") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_domain_manager_edit(self): """Checking that if a user is a domain manager they can edit the Domain Manager portion of the Renewal Form.""" @@ -677,7 +669,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): self.assertEqual(edit_page.status_code, 200) self.assertContains(edit_page, "Domain managers can update all information related to a domain") - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_not_expired_or_expiring(self): """Checking that if the user's domain is not expired or expiring that user should not be able to access /renewal and that it should receive a 403.""" @@ -686,7 +677,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_not_expiring.id})) self.assertEqual(renewal_page.status_code, 403) - @override_flag("domain_renewal", active=True) def test_domain_renewal_form_does_not_appear_if_not_domain_manager(self): """If user is not a domain manager and tries to access /renewal, user should receive a 403.""" with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object( @@ -695,7 +685,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_no_domain_manager.id})) self.assertEqual(renewal_page.status_code, 403) - @override_flag("domain_renewal", active=True) def test_ack_checkbox_not_checked(self): """If user don't check the checkbox, user should receive an error message.""" # Grab the renewal URL @@ -707,7 +696,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview): error_message = "Check the box if you read and agree to the requirements for operating a .gov domain." self.assertContains(response, error_message) - @override_flag("domain_renewal", active=True) def test_ack_checkbox_checked(self): """If user check the checkbox and submits the form, user should be redirected Domain Over page with an updated by 1 year expiration date""" @@ -2966,26 +2954,15 @@ class TestDomainRenewal(TestWithUser): pass super().tearDown() - # Remove test_without_domain_renewal_flag when domain renewal is released as a feature @less_console_noise_decorator - @override_flag("domain_renewal", active=False) - def test_without_domain_renewal_flag(self): - self.client.force_login(self.user) - domains_page = self.client.get("/") - self.assertNotContains(domains_page, "will expire soon") - self.assertNotContains(domains_page, "Expiring soon") - - @less_console_noise_decorator - @override_flag("domain_renewal", active=True) - def test_domain_renewal_flag_single_domain(self): + def test_domain_with_single_domain(self): self.client.force_login(self.user) domains_page = self.client.get("/") self.assertContains(domains_page, "One domain will expire soon") self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) - def test_with_domain_renewal_flag_mulitple_domains(self): + def test_with_mulitple_domains(self): today = datetime.now() expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d") self.domain_with_another_expiring, _ = Domain.objects.get_or_create( @@ -3001,8 +2978,7 @@ class TestDomainRenewal(TestWithUser): self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) - def test_with_domain_renewal_flag_no_expiring_domains(self): + def test_with_no_expiring_domains(self): UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete() UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete() self.client.force_login(self.user) @@ -3010,18 +2986,16 @@ class TestDomainRenewal(TestWithUser): self.assertNotContains(domains_page, "will expire soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) - def test_domain_renewal_flag_single_domain_w_org_feature_flag(self): + def test_single_domain_w_org_feature_flag(self): self.client.force_login(self.user) domains_page = self.client.get("/") self.assertContains(domains_page, "One domain will expire soon") self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) - def test_with_domain_renewal_flag_mulitple_domains_w_org_feature_flag(self): + def test_with_mulitple_domains_w_org_feature_flag(self): today = datetime.now() expiring_date = (today + timedelta(days=31)).strftime("%Y-%m-%d") self.domain_with_another_expiring_org_model, _ = Domain.objects.get_or_create( @@ -3037,9 +3011,8 @@ class TestDomainRenewal(TestWithUser): self.assertContains(domains_page, "Expiring soon") @less_console_noise_decorator - @override_flag("domain_renewal", active=True) @override_flag("organization_feature", active=True) - def test_with_domain_renewal_flag_no_expiring_domains_w_org_feature_flag(self): + def test_no_expiring_domains_w_org_feature_flag(self): UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete() UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete() self.client.force_login(self.user) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 297cb689a..27ee44068 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -366,7 +366,7 @@ class DomainRenewalView(DomainBaseView): return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk})) # if not valid, render the template with error messages - # passing editable, has_domain_renewal_flag, and is_editable for re-render + # passing editable,å and is_editable for re-render return render( request, "domain_renewal.html", @@ -374,7 +374,6 @@ class DomainRenewalView(DomainBaseView): "domain": domain, "form": form, "is_editable": True, - "has_domain_renewal_flag": True, "is_domain_manager": True, }, ) From 2a6d401e9b23c2c8d22bedf8282903b55f3ea6ee Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:00:32 -0700 Subject: [PATCH 22/50] Move get-gov-reports to src/ @rachidatecs see above ^ --- src/registrar/assets/js/get-gov-reports.js | 203 ------------------ .../assets/src/js/getgov-admin/analytics.js | 196 +++++++++++++++++ .../src/js/getgov-admin/helpers-admin.js | 10 + .../assets/src/js/getgov-admin/main.js | 4 + src/registrar/templates/admin/analytics.html | 14 +- src/registrar/templates/admin/base_site.html | 1 - 6 files changed, 217 insertions(+), 211 deletions(-) delete mode 100644 src/registrar/assets/js/get-gov-reports.js create mode 100644 src/registrar/assets/src/js/getgov-admin/analytics.js diff --git a/src/registrar/assets/js/get-gov-reports.js b/src/registrar/assets/js/get-gov-reports.js deleted file mode 100644 index 54cdc789a..000000000 --- a/src/registrar/assets/js/get-gov-reports.js +++ /dev/null @@ -1,203 +0,0 @@ - -/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button, - * attach the seleted start and end dates to a url that'll trigger the view, and finally - * redirect to that url. - * - * This function also sets the start and end dates to match the url params if they exist -*/ -(function () { - // Function to get URL parameter value by name - function getParameterByName(name, url) { - if (!url) url = window.location.href; - name = name.replace(/[\[\]]/g, '\\$&'); - var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), - results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - } - - // Get the current date in the format YYYY-MM-DD - let currentDate = new Date().toISOString().split('T')[0]; - - // Default the value of the start date input field to the current date - let startDateInput = document.getElementById('start'); - - // Default the value of the end date input field to the current date - let endDateInput = document.getElementById('end'); - - let exportButtons = document.querySelectorAll('.exportLink'); - - if (exportButtons.length > 0) { - // Check if start and end dates are present in the URL - let urlStartDate = getParameterByName('start_date'); - let urlEndDate = getParameterByName('end_date'); - - // Set input values based on URL parameters or current date - startDateInput.value = urlStartDate || currentDate; - endDateInput.value = urlEndDate || currentDate; - - exportButtons.forEach((btn) => { - btn.addEventListener('click', function () { - // Get the selected start and end dates - let startDate = startDateInput.value; - let endDate = endDateInput.value; - let exportUrl = btn.dataset.exportUrl; - - // Build the URL with parameters - exportUrl += "?start_date=" + startDate + "&end_date=" + endDate; - - // Redirect to the export URL - window.location.href = exportUrl; - }); - }); - } - -})(); - - -/** An IIFE to initialize the analytics page -*/ -(function () { - const chartInstances = new Map(); - - /** - * Creates a diagonal stripe pattern for chart.js - * Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns - * and https://github.com/ashiguruma/patternomaly - * @param {string} backgroundColor - Background color of the pattern - * @param {string} [lineColor="white"] - Color of the diagonal lines - * @param {boolean} [rightToLeft=false] - Direction of the diagonal lines - * @param {number} [lineGap=1] - Gap between lines - * @returns {CanvasPattern} A canvas pattern object for use with backgroundColor - */ - function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, lineGap=1) { - // Define the canvas and the 2d context so we can draw on it - let shape = document.createElement("canvas"); - shape.width = 20; - shape.height = 20; - let context = shape.getContext("2d"); - - // Fill with specified background color - context.fillStyle = backgroundColor; - context.fillRect(0, 0, shape.width, shape.height); - - // Set stroke properties - context.strokeStyle = lineColor; - context.lineWidth = 2; - - // Rotate canvas for a right-to-left pattern - if (rightToLeft) { - context.translate(shape.width, 0); - context.rotate(90 * Math.PI / 180); - }; - - // First diagonal line - let halfSize = shape.width / 2; - context.moveTo(halfSize - lineGap, -lineGap); - context.lineTo(shape.width + lineGap, halfSize + lineGap); - - // Second diagonal line (x,y are swapped) - context.moveTo(-lineGap, halfSize - lineGap); - context.lineTo(halfSize + lineGap, shape.width + lineGap); - - context.stroke(); - return context.createPattern(shape, "repeat"); - } - - function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) { - var canvas = document.getElementById(canvasId); - if (!canvas) { - return - } - - var ctx = canvas.getContext("2d"); - - var listOne = JSON.parse(canvas.getAttribute('data-list-one')); - var listTwo = JSON.parse(canvas.getAttribute('data-list-two')); - - var data = { - labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"], - datasets: [ - { - label: labelOne, - backgroundColor: "rgba(255, 99, 132, 0.3)", - borderColor: "rgba(255, 99, 132, 1)", - borderWidth: 1, - data: listOne, - // Set this line style to be rightToLeft for visual distinction - backgroundColor: createDiagonalPattern('rgba(255, 99, 132, 0.3)', 'white', true) - }, - { - label: labelTwo, - backgroundColor: "rgba(75, 192, 192, 0.3)", - borderColor: "rgba(75, 192, 192, 1)", - borderWidth: 1, - data: listTwo, - backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)', 'white') - }, - ], - }; - - var options = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top', - }, - title: { - display: true, - text: title - } - }, - scales: { - y: { - beginAtZero: true, - }, - }, - }; - - if (chartInstances.has(canvasId)) { - chartInstances.get(canvasId).destroy(); - } - - const chart = new Chart(ctx, { - type: "bar", - data: data, - options: options, - }); - - chartInstances.set(canvasId, chart); - } - - function handleResize() { - // Debounce the resize handler - if (handleResize.timeout) { - clearTimeout(handleResize.timeout); - } - - handleResize.timeout = setTimeout(() => { - chartInstances.forEach((chart, canvasId) => { - if (chart && chart.canvas) { - chart.resize(); - } - }); - }, 100); - } - - function initComparativeColumnCharts() { - document.addEventListener("DOMContentLoaded", function () { - createComparativeColumnChart("myChart1", "Managed domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart2", "Unmanaged domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart3", "Deleted domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart4", "Ready domains", "Start Date", "End Date"); - createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date"); - createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date"); - - window.addEventListener("resize", handleResize); - }); - }; - - initComparativeColumnCharts(); -})(); diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js new file mode 100644 index 000000000..d2808f623 --- /dev/null +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -0,0 +1,196 @@ + +import { debounce } from '../getgov/helpers.js'; +import { getParameterByName } from './helpers-admin.js'; + + +/** This function also sets the start and end dates to match the url params if they exist +*/ +function initAnalyticsExportButtons() { + // Get the current date in the format YYYY-MM-DD + let currentDate = new Date().toISOString().split('T')[0]; + + // Default the value of the start date input field to the current date + let startDateInput = document.getElementById('start'); + + // Default the value of the end date input field to the current date + let endDateInput = document.getElementById('end'); + + let exportButtons = document.querySelectorAll('.exportLink'); + + if (exportButtons.length > 0) { + // Check if start and end dates are present in the URL + let urlStartDate = getParameterByName('start_date'); + let urlEndDate = getParameterByName('end_date'); + + // Set input values based on URL parameters or current date + startDateInput.value = urlStartDate || currentDate; + endDateInput.value = urlEndDate || currentDate; + + exportButtons.forEach((btn) => { + btn.addEventListener('click', function () { + // Get the selected start and end dates + let startDate = startDateInput.value; + let endDate = endDateInput.value; + let exportUrl = btn.dataset.exportUrl; + + // Build the URL with parameters + exportUrl += "?start_date=" + startDate + "&end_date=" + endDate; + + // Redirect to the export URL + window.location.href = exportUrl; + }); + }); + } + +}; + +/** + * Creates a diagonal stripe pattern for chart.js + * Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns + * and https://github.com/ashiguruma/patternomaly + * @param {string} backgroundColor - Background color of the pattern + * @param {string} [lineColor="white"] - Color of the diagonal lines + * @param {boolean} [rightToLeft=false] - Direction of the diagonal lines + * @param {number} [lineGap=1] - Gap between lines + * @returns {CanvasPattern} A canvas pattern object for use with backgroundColor + */ +function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, lineGap=1) { + // Define the canvas and the 2d context so we can draw on it + let shape = document.createElement("canvas"); + shape.width = 20; + shape.height = 20; + let context = shape.getContext("2d"); + + // Fill with specified background color + context.fillStyle = backgroundColor; + context.fillRect(0, 0, shape.width, shape.height); + + // Set stroke properties + context.strokeStyle = lineColor; + context.lineWidth = 2; + + // Rotate canvas for a right-to-left pattern + if (rightToLeft) { + context.translate(shape.width, 0); + context.rotate(90 * Math.PI / 180); + }; + + // First diagonal line + let halfSize = shape.width / 2; + context.moveTo(halfSize - lineGap, -lineGap); + context.lineTo(shape.width + lineGap, halfSize + lineGap); + + // Second diagonal line (x,y are swapped) + context.moveTo(-lineGap, halfSize - lineGap); + context.lineTo(halfSize + lineGap, shape.width + lineGap); + + context.stroke(); + return context.createPattern(shape, "repeat"); +} + +function createComparativeColumnChart(canvasId, title, labelOne, labelTwo, chartInstances) { + var canvas = document.getElementById(canvasId); + if (!canvas) { + return + } + + var ctx = canvas.getContext("2d"); + + var listOne = JSON.parse(canvas.getAttribute('data-list-one')); + var listTwo = JSON.parse(canvas.getAttribute('data-list-two')); + + var data = { + labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"], + datasets: [ + { + label: labelOne, + backgroundColor: "rgba(255, 99, 132, 0.3)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + data: listOne, + // Set this line style to be rightToLeft for visual distinction + backgroundColor: createDiagonalPattern('rgba(255, 99, 132, 0.3)', 'white', true) + }, + { + label: labelTwo, + backgroundColor: "rgba(75, 192, 192, 0.3)", + borderColor: "rgba(75, 192, 192, 1)", + borderWidth: 1, + data: listTwo, + backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)', 'white') + }, + ], + }; + + var options = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: title + } + }, + scales: { + y: { + beginAtZero: true, + }, + }, + }; + + if (chartInstances.has(canvasId)) { + chartInstances.get(canvasId).destroy(); + } + + const chart = new Chart(ctx, { + type: "bar", + data: data, + options: options, + }); + + chartInstances.set(canvasId, chart); +} + +function initComparativeColumnCharts(chartInstances) { + // Create charts + const charts = [ + { id: "managed-domains-chart", title: "Managed domains" }, + { id: "unmanaged-domains-chart", title: "Unmanaged domains" }, + { id: "deleted-domains-chart", title: "Deleted domains" }, + { id: "ready-domains-chart", title: "Ready domains" }, + { id: "submitted-requests-chart", title: "Submitted requests" }, + { id: "all-requests-chart", title: "All requests" } + ]; + charts.forEach(chart => { + createComparativeColumnChart( + chart.id, + chart.title, + "Start Date", + "End Date", + chartInstances + ); + }); + + // Add resize listener to each chart + window.addEventListener("resize", debounce(() => { + chartInstances.forEach((chart) => { + if (chart?.canvas) chart.resize(); + }); + }, 200)); +}; + +/** An IIFE to initialize the analytics page +*/ +export function initAnalyticsDashboard() { + const chartInstances = new Map(); + const analyticsPageContainer = document.querySelector('.analytics-dashboard .analytics-dashboard-charts'); + if (analyticsPageContainer) { + document.addEventListener("DOMContentLoaded", function () { + initAnalyticsExportButtons(); + initComparativeColumnCharts(chartInstances); + }); + } +}; diff --git a/src/registrar/assets/src/js/getgov-admin/helpers-admin.js b/src/registrar/assets/src/js/getgov-admin/helpers-admin.js index ff618a67d..8055e29d3 100644 --- a/src/registrar/assets/src/js/getgov-admin/helpers-admin.js +++ b/src/registrar/assets/src/js/getgov-admin/helpers-admin.js @@ -22,3 +22,13 @@ export function addOrRemoveSessionBoolean(name, add){ sessionStorage.removeItem(name); } } + +export function getParameterByName(name, url) { + if (!url) url = window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} diff --git a/src/registrar/assets/src/js/getgov-admin/main.js b/src/registrar/assets/src/js/getgov-admin/main.js index 64be572b2..5c6de20ab 100644 --- a/src/registrar/assets/src/js/getgov-admin/main.js +++ b/src/registrar/assets/src/js/getgov-admin/main.js @@ -15,6 +15,7 @@ import { initDomainFormTargetBlankButtons } from './domain-form.js'; import { initDynamicPortfolioFields } from './portfolio-form.js'; import { initDynamicDomainInformationFields } from './domain-information-form.js'; import { initDynamicDomainFields } from './domain-form.js'; +import { initAnalyticsDashboard } from './analytics.js'; // General initModals(); @@ -41,3 +42,6 @@ initDynamicPortfolioFields(); // Domain information initDynamicDomainInformationFields(); + +// Analytics dashboard +initAnalyticsDashboard(); diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index ca3501eec..d345aeb14 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -18,7 +18,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ {% block content %} -
+
@@ -136,7 +136,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% comment %} Managed/Unmanaged domains {% endcomment %}
-
- -
- -
- - {% endblock %} From b3a3dcad6dcfe15331f3da2dba69af58c450dce0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:31:01 -0700 Subject: [PATCH 23/50] Cleanup + code simplification --- .../assets/src/js/getgov-admin/analytics.js | 67 +++++++------------ .../admin/analytics_graph_table.html | 2 +- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index d2808f623..e2de7b247 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -41,7 +41,6 @@ function initAnalyticsExportButtons() { }); }); } - }; /** @@ -88,8 +87,8 @@ function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, li return context.createPattern(shape, "repeat"); } -function createComparativeColumnChart(canvasId, title, labelOne, labelTwo, chartInstances) { - var canvas = document.getElementById(canvasId); +function createComparativeColumnChart(id, title, labelOne, labelTwo) { + var canvas = document.getElementById(id); if (!canvas) { return } @@ -140,57 +139,43 @@ function createComparativeColumnChart(canvasId, title, labelOne, labelTwo, chart }, }, }; - - if (chartInstances.has(canvasId)) { - chartInstances.get(canvasId).destroy(); - } - - const chart = new Chart(ctx, { + return new Chart(ctx, { type: "bar", data: data, options: options, }); - - chartInstances.set(canvasId, chart); } -function initComparativeColumnCharts(chartInstances) { - // Create charts - const charts = [ - { id: "managed-domains-chart", title: "Managed domains" }, - { id: "unmanaged-domains-chart", title: "Unmanaged domains" }, - { id: "deleted-domains-chart", title: "Deleted domains" }, - { id: "ready-domains-chart", title: "Ready domains" }, - { id: "submitted-requests-chart", title: "Submitted requests" }, - { id: "all-requests-chart", title: "All requests" } - ]; - charts.forEach(chart => { - createComparativeColumnChart( - chart.id, - chart.title, - "Start Date", - "End Date", - chartInstances - ); - }); - - // Add resize listener to each chart - window.addEventListener("resize", debounce(() => { - chartInstances.forEach((chart) => { - if (chart?.canvas) chart.resize(); - }); - }, 200)); -}; - /** An IIFE to initialize the analytics page */ export function initAnalyticsDashboard() { - const chartInstances = new Map(); const analyticsPageContainer = document.querySelector('.analytics-dashboard .analytics-dashboard-charts'); if (analyticsPageContainer) { document.addEventListener("DOMContentLoaded", function () { initAnalyticsExportButtons(); - initComparativeColumnCharts(chartInstances); + + // Create charts and store each instance of it + const chartInstances = new Map(); + const charts = [ + { id: "managed-domains-chart", title: "Managed domains" }, + { id: "unmanaged-domains-chart", title: "Unmanaged domains" }, + { id: "deleted-domains-chart", title: "Deleted domains" }, + { id: "ready-domains-chart", title: "Ready domains" }, + { id: "submitted-requests-chart", title: "Submitted requests" }, + { id: "all-requests-chart", title: "All requests" } + ]; + charts.forEach(chart => { + if (chartInstances.has(chart.id)) chartInstances.get(chart.id).destroy(); + let chart = createComparativeColumnChart(...chart, "Start Date", "End Date"); + chartInstances.set(chart.id, chart); + }); + + // Add resize listener to each chart + window.addEventListener("resize", debounce(() => { + chartInstances.forEach((chart) => { + if (chart?.canvas) chart.resize(); + }); + }, 200)); }); } }; diff --git a/src/registrar/templates/admin/analytics_graph_table.html b/src/registrar/templates/admin/analytics_graph_table.html index 88b538745..5f10da93a 100644 --- a/src/registrar/templates/admin/analytics_graph_table.html +++ b/src/registrar/templates/admin/analytics_graph_table.html @@ -23,4 +23,4 @@ {% endfor %} {% endwith %}
-
TypeStart date {{ data.start_date }}End date {{ data.end_date }} TypeStart date {{ data.start_date }}End date {{ data.end_date }}
\ No newline at end of file + From 62a8a0c2a1e5fbd6cc72de262d0267c566a89681 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:34:38 -0700 Subject: [PATCH 24/50] Update analytics.js --- src/registrar/assets/src/js/getgov-admin/analytics.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index e2de7b247..7bf6a05b8 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -1,8 +1,6 @@ - import { debounce } from '../getgov/helpers.js'; import { getParameterByName } from './helpers-admin.js'; - /** This function also sets the start and end dates to match the url params if they exist */ function initAnalyticsExportButtons() { @@ -94,7 +92,6 @@ function createComparativeColumnChart(id, title, labelOne, labelTwo) { } var ctx = canvas.getContext("2d"); - var listOne = JSON.parse(canvas.getAttribute('data-list-one')); var listTwo = JSON.parse(canvas.getAttribute('data-list-two')); From 8a0fdc7ea637053c532d387d58c0afef0698551b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:44:22 -0700 Subject: [PATCH 25/50] Update test_reports.py --- src/registrar/tests/test_reports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index bab4f327b..18c98807d 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -1,4 +1,5 @@ import io +from unittest import skip from django.test import Client, RequestFactory from io import StringIO from registrar.models import ( @@ -819,6 +820,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): super().setUp() self.factory = RequestFactory() + @skip("flaky test that needs to be refactored") @override_flag("organization_feature", active=True) @override_flag("organization_members", active=True) @less_console_noise_decorator From 8acb36ef9ad15989608ab98dc15ba355f2f53761 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:46:14 -0700 Subject: [PATCH 26/50] remove unrelated changes --- src/.pa11yci | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index ed382a38a..571d0b1c8 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -8,20 +8,20 @@ "http://localhost:8080/health/", "http://localhost:8080/request/", "http://localhost:8080/request/start", - "http://localhost:8080/request/1/organization/", - "http://localhost:8080/request/1/org_federal/", - "http://localhost:8080/request/1/org_election/", - "http://localhost:8080/request/1/org_contact/", - "http://localhost:8080/request/1/senior_official/", - "http://localhost:8080/request/1/current_sites/", - "http://localhost:8080/request/1/dotgov_domain/", - "http://localhost:8080/request/1/purpose/", - "http://localhost:8080/request/1/your_contact/", - "http://localhost:8080/request/1/other_contacts/", - "http://localhost:8080/request/1/anything_else/", - "http://localhost:8080/request/1/requirements/", - "http://localhost:8080/request/1/finished/", - "http://localhost:8080/request/1/requesting_entity/", + "http://localhost:8080/request/organization/", + "http://localhost:8080/request/org_federal/", + "http://localhost:8080/request/org_election/", + "http://localhost:8080/request/org_contact/", + "http://localhost:8080/request/senior_official/", + "http://localhost:8080/request/current_sites/", + "http://localhost:8080/request/dotgov_domain/", + "http://localhost:8080/request/purpose/", + "http://localhost:8080/request/your_contact/", + "http://localhost:8080/request/other_contacts/", + "http://localhost:8080/request/anything_else/", + "http://localhost:8080/request/requirements/", + "http://localhost:8080/request/finished/", + "http://localhost:8080/request/requesting_entity/", "http://localhost:8080/user-profile/", "http://localhost:8080/members/", "http://localhost:8080/members/new-member" From c540a324c7df33c7011fcef4bebe158a43bf417a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:11:25 -0800 Subject: [PATCH 27/50] Add unit tests --- src/registrar/tests/test_emails.py | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index f39f11517..c79038668 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -108,6 +108,82 @@ class TestEmails(TestCase): self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"]) + @boto3_mocking.patching + @override_settings(IS_PRODUCTION=True, BASE_URL="manage.get.gov") + def test_email_production_subject_and_url_check(self): + """Test sending an email in production that: + 1. Does not have a prefix in the email subject (no [MANAGE]) + 2. Uses the production URL in the email body of manage.get.gov still""" + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + send_templated_email( + "emails/update_to_approved_domain.txt", + "emails/update_to_approved_domain_subject.txt", + "doesnotexist@igorville.com", + context={"domain": "test", "user": "test", "date": 1, "changes": "test"}, + bcc_address=None, + cc_addresses=["testy2@town.com", "mayor@igorville.gov"], + ) + + # check that an email was sent + self.assertTrue(self.mock_client.send_email.called) + + # check the call sequence for the email + args, kwargs = self.mock_client.send_email.call_args + self.assertIn("Destination", kwargs) + self.assertIn("CcAddresses", kwargs["Destination"]) + + self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"]) + + # Grab email subject + email_subject = kwargs["Content"]["Simple"]["Subject"]["Data"] + + # Check that the subject does NOT contain a prefix for production + self.assertNotIn("[MANAGE]", email_subject) + self.assertIn("An update was made to", email_subject) + + # Grab email body + email_body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + # Check that manage_url is correctly set for production + self.assertIn("https://manage.get.gov", email_body) + + @boto3_mocking.patching + @override_settings(IS_PRODUCTION=False, BASE_URL="https://getgov-rh.app.cloud.gov") + def test_email_non_production_subject_and_url_check(self): + """Test sending an email in production that: + 1. Does prefix in the email subject ([GETGOV-RH]) + 2. Uses the sandbox url in the email body (ie getgov-rh.app.cloud.gov)""" + with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): + send_templated_email( + "emails/update_to_approved_domain.txt", + "emails/update_to_approved_domain_subject.txt", + "doesnotexist@igorville.com", + context={"domain": "test", "user": "test", "date": 1, "changes": "test"}, + bcc_address=None, + cc_addresses=["testy2@town.com", "mayor@igorville.gov"], + ) + + # check that an email was sent + self.assertTrue(self.mock_client.send_email.called) + + # check the call sequence for the email + args, kwargs = self.mock_client.send_email.call_args + self.assertIn("Destination", kwargs) + self.assertIn("CcAddresses", kwargs["Destination"]) + self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"]) + + # Grab email subject + email_subject = kwargs["Content"]["Simple"]["Subject"]["Data"] + + # Check that the subject DOES contain a prefix of the current sandbox + self.assertIn("[GETGOV-RH]", email_subject) + + # Grab email body + email_body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + + # Check that manage_url is correctly set of the sandbox + self.assertIn("https://getgov-rh.app.cloud.gov", email_body) + @boto3_mocking.patching @less_console_noise_decorator def test_submission_confirmation(self): From 0454e7295136eedc10a3190f09bc90b95ca899f2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:23:37 -0700 Subject: [PATCH 28/50] fix typo --- src/registrar/assets/src/js/getgov-admin/analytics.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index 7bf6a05b8..a5488fa3d 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -163,8 +163,7 @@ export function initAnalyticsDashboard() { ]; charts.forEach(chart => { if (chartInstances.has(chart.id)) chartInstances.get(chart.id).destroy(); - let chart = createComparativeColumnChart(...chart, "Start Date", "End Date"); - chartInstances.set(chart.id, chart); + chartInstances.set(chart.id, createComparativeColumnChart(...chart, "Start Date", "End Date")); }); // Add resize listener to each chart From 0725ab8e59e59ac63267fe1d008998d7fd7ba26c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:31:53 -0800 Subject: [PATCH 29/50] Clean up the comments --- src/registrar/tests/test_emails.py | 2 +- src/registrar/utility/email.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index c79038668..2b7f89ac9 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -151,7 +151,7 @@ class TestEmails(TestCase): @override_settings(IS_PRODUCTION=False, BASE_URL="https://getgov-rh.app.cloud.gov") def test_email_non_production_subject_and_url_check(self): """Test sending an email in production that: - 1. Does prefix in the email subject ([GETGOV-RH]) + 1. Does prefix in the email subject (ie [GETGOV-RH]) 2. Uses the sandbox url in the email body (ie getgov-rh.app.cloud.gov)""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): send_templated_email( diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 535096b10..9323255af 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -57,7 +57,7 @@ def send_templated_email( # noqa env_name = re.sub(r"^https?://", "", env_base_url).split(".")[0] # To add to subject lines ie [GETGOV-RH] prefix = f"[{env_name.upper()}] " if not settings.IS_PRODUCTION else "" - # For email links + # For email links ie getgov-rh.app.cloud.gov manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" # Adding to context From 00732d0a64499224cc1e7da96ec35eba54970d51 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 10:58:38 -0800 Subject: [PATCH 30/50] Fix carrot link --- src/registrar/templates/emails/domain_manager_notification.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/domain_manager_notification.txt b/src/registrar/templates/emails/domain_manager_notification.txt index 18e682329..b5096a9d8 100644 --- a/src/registrar/templates/emails/domain_manager_notification.txt +++ b/src/registrar/templates/emails/domain_manager_notification.txt @@ -15,7 +15,7 @@ The person who received the invitation will become a domain manager once they lo associated with the invited email address. If you need to cancel this invitation or remove the domain manager, you can do that by going to -this domain in the .gov registrar <{{ manage_url }}. +this domain in the .gov registrar <{{ manage_url }}>. WHY DID YOU RECEIVE THIS EMAIL? From 16f0ae6f627417f9162c29bfa7b832396e4c5951 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 12 Feb 2025 11:00:13 -0800 Subject: [PATCH 31/50] Fix more spacing --- .../emails/portfolio_admin_removal_notification_subject.txt | 2 +- src/registrar/templates/emails/update_to_approved_domain.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt index 030d27ae7..9a45a8bbc 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt @@ -1 +1 @@ -{{ prefix}}An admin was removed from your .gov organization \ No newline at end of file +{{ prefix }}An admin was removed from your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/update_to_approved_domain.txt b/src/registrar/templates/emails/update_to_approved_domain.txt index fb0a442cb..070096f62 100644 --- a/src/registrar/templates/emails/update_to_approved_domain.txt +++ b/src/registrar/templates/emails/update_to_approved_domain.txt @@ -1,4 +1,4 @@ - {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi, An update was made to a domain you manage. From 53674dea6ab674ad6a491e73a2c7f7c85ef4b9b4 Mon Sep 17 00:00:00 2001 From: lizpearl Date: Wed, 12 Feb 2025 15:20:38 -0600 Subject: [PATCH 32/50] Add Jaxon to fixtures --- src/registrar/fixtures/fixtures_users.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index 876bc9fb5..fdaa1c135 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -171,6 +171,13 @@ class UserFixture: "email": "gina.summers@ecstech.com", "title": "Scrum Master", }, + { + "username": "89f2db87-87a2-4778-a5ea-5b27b585b131", + "first_name": "Jaxon", + "last_name": "Silva", + "email": "jaxon.silva@cisa.dhs.gov", + "title": "Designer", + }, ] STAFF = [ From 6a44fe97aa7baf573ac19cb5a2996bd453d415b9 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 12 Feb 2025 22:05:23 -0500 Subject: [PATCH 33/50] fixed pr comments --- src/registrar/models/domain.py | 2 +- src/registrar/views/domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 245f869ce..d92da8832 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1172,7 +1172,7 @@ class Domain(TimeStampedModel, DomainHelper): """Return the display status of the domain.""" if self.is_expired() and (self.state != self.State.UNKNOWN): return "Expired" - elif flag_is_active(request, "domain_renewal") and self.is_expiring(): + elif self.is_expiring(): return "Expiring soon" elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED: return "DNS needed" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b6cabfcf6..72826e570 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -366,7 +366,7 @@ class DomainRenewalView(DomainBaseView): return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk})) # if not valid, render the template with error messages - # passing editable,å and is_editable for re-render + # passing editable and is_editable for re-render return render( request, "domain_renewal.html", From 3245cded0e81597974496799eace8a3ee6835969 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 13 Feb 2025 10:58:40 -0500 Subject: [PATCH 34/50] updated naming for workflow --- .github/workflows/delete-and-recreate-db.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/delete-and-recreate-db.yaml b/.github/workflows/delete-and-recreate-db.yaml index ecdf54bbc..e7cad783d 100644 --- a/.github/workflows/delete-and-recreate-db.yaml +++ b/.github/workflows/delete-and-recreate-db.yaml @@ -1,8 +1,8 @@ # This workflow can be run from the CLI # gh workflow run reset-db.yaml -f environment=ENVIRONMENT -name: Reset database -run-name: Reset database for ${{ github.event.inputs.environment }} +name: Delete and Recreate database +run-name: Delete and Recreate for ${{ github.event.inputs.environment }} on: workflow_dispatch: From 71a9e9515d99d5e4481c1993379bbeb74c5b178f Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 13 Feb 2025 11:09:32 -0500 Subject: [PATCH 35/50] updated the creds variable --- .github/workflows/delete-and-recreate-db.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/delete-and-recreate-db.yaml b/.github/workflows/delete-and-recreate-db.yaml index e7cad783d..979f20826 100644 --- a/.github/workflows/delete-and-recreate-db.yaml +++ b/.github/workflows/delete-and-recreate-db.yaml @@ -53,7 +53,7 @@ jobs: sudo apt-get update sudo apt-get install cf8-cli cf api api.fr.cloud.gov - cf auth "$CF_USERNAME" "$CF_PASSWORD" + cf auth "$cf_username" "$cf_password" cf target -o cisa-dotgov -s $DESTINATION_ENVIRONMENT From a928cc742c221579a955de8b9261f3b98335dbc8 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 13 Feb 2025 12:18:55 -0500 Subject: [PATCH 36/50] updated changes --- src/registrar/context_processors.py | 8 -------- src/registrar/models/domain.py | 1 - 2 files changed, 9 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 061c0ab4f..4e17b7fa1 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -71,14 +71,6 @@ def portfolio_permissions(request): } try: portfolio = request.session.get("portfolio") - - # These feature flags will display and doesn't depend on portfolio - portfolio_context.update( - { - "has_organization_feature_flag": True, - } - ) - if portfolio: return { "has_view_portfolio_permission": request.user.has_view_portfolio_permission(portfolio), diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index d92da8832..42310c3bb 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -41,7 +41,6 @@ from .utility.time_stamped_model import TimeStampedModel from .public_contact import PublicContact from .user_domain_role import UserDomainRole -from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) From c737daa8fd2f8d497edfb9c5499d74e66316dcd7 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 13 Feb 2025 10:28:49 -0800 Subject: [PATCH 37/50] Add prefix and manage url to the action needed reasons templates --- .../action_needed_reasons/already_has_domains_subject.txt | 2 +- .../templates/emails/action_needed_reasons/bad_name.txt | 2 +- .../templates/emails/action_needed_reasons/bad_name_subject.txt | 2 +- .../action_needed_reasons/eligibility_unclear_subject.txt | 2 +- .../action_needed_reasons/questionable_senior_official.txt | 2 +- .../questionable_senior_official_subject.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt index ac563b549..40e5ed899 100644 --- a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt +++ b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt @@ -17,7 +17,7 @@ Domains should uniquely identify a government organization and be clear to the g ACTION NEEDED -First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. Once you submit your updated request, we’ll resume the adjudication process. +First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. <{{ manage_url }}> Once you submit your updated request, we’ll resume the adjudication process. If you have questions or want to discuss potential domain names, reply to this email. diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt index ef05e17d7..40d068cd9 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt @@ -21,7 +21,7 @@ We expect a senior official to be someone in a role of significant, executive re ACTION NEEDED Reply to this email with a justification for naming {{ domain_request.senior_official.get_formatted_name }} as the senior official. If you have questions or comments, include those in your reply. -Alternatively, you can log in to the registrar and enter a different senior official for this domain request. Once you submit your updated request, we’ll resume the adjudication process. +Alternatively, you can log in to the registrar and enter a different senior official for this domain request. <{{ manage_url }}> Once you submit your updated request, we’ll resume the adjudication process. THANK YOU diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt index 7ca332ddd..b29b8040c 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt @@ -1 +1 @@ -Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file From 856b71b6ce70e429a77a1342e20ac41db8a75b42 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 13 Feb 2025 10:30:48 -0800 Subject: [PATCH 38/50] Add for transition domain inv email --- src/registrar/templates/emails/transition_domain_invitation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt index dc812edf3..14dd626dd 100644 --- a/src/registrar/templates/emails/transition_domain_invitation.txt +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -57,7 +57,7 @@ THANK YOU The .gov team .Gov blog -Domain management +Domain management <{{ manage_url }}}> Get.gov The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) From 986bcc235125f627c6458a4b36b5d40ad227e106 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 14 Feb 2025 06:56:56 -0500 Subject: [PATCH 39/50] fixed date in email --- src/registrar/utility/email_invitations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index d206bf279..7ddab65f1 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -255,6 +255,7 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo "portfolio": permissions.portfolio, "requestor_email": requestor_email, "permissions": permissions, + "date": date.today(), }, ) except EmailSendingError: From 8020bd6f7461b71fa663fc5fff8c4a797eeed907 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 08:02:47 -0800 Subject: [PATCH 40/50] Update for prefix to be in the subject dynamically --- .../already_has_domains_subject.txt | 2 +- .../emails/action_needed_reasons/bad_name_subject.txt | 2 +- .../eligibility_unclear_subject.txt | 2 +- .../questionable_senior_official_subject.txt | 2 +- .../templates/emails/domain_invitation_subject.txt | 2 +- .../domain_manager_deleted_notification_subject.txt | 2 +- .../emails/domain_manager_notification_subject.txt | 2 +- .../emails/domain_request_withdrawn_subject.txt | 2 +- src/registrar/templates/emails/metadata_body.txt | 2 +- src/registrar/templates/emails/metadata_subject.txt | 2 +- .../portfolio_admin_addition_notification_subject.txt | 2 +- .../portfolio_admin_removal_notification_subject.txt | 2 +- .../templates/emails/portfolio_invitation_subject.txt | 2 +- .../emails/status_change_approved_subject.txt | 2 +- .../templates/emails/status_change_subject.txt | 2 +- .../emails/submission_confirmation_subject.txt | 2 +- .../emails/transition_domain_invitation_subject.txt | 2 +- .../emails/update_to_approved_domain_subject.txt | 2 +- src/registrar/utility/email.py | 11 +++++++---- 19 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/already_has_domains_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/bad_name_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt index b29b8040c..7ca332ddd 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file +Update on your .gov request: {{ domain_request.requested_domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_invitation_subject.txt b/src/registrar/templates/emails/domain_invitation_subject.txt index 9f15c38b4..9663346d0 100644 --- a/src/registrar/templates/emails/domain_invitation_subject.txt +++ b/src/registrar/templates/emails/domain_invitation_subject.txt @@ -1 +1 @@ -{{ prefix }}You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file +You've been invited to manage {% if domains|length > 1 %}.gov domains{% else %}{{ domains.0.name }}{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt index 7376bdb86..c84a20f18 100644 --- a/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_deleted_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}A domain manager was removed from {{ domain.name }} \ No newline at end of file +A domain manager was removed from {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_manager_notification_subject.txt b/src/registrar/templates/emails/domain_manager_notification_subject.txt index 8560cb9fa..0e9918de0 100644 --- a/src/registrar/templates/emails/domain_manager_notification_subject.txt +++ b/src/registrar/templates/emails/domain_manager_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}A domain manager was invited to {{ domain.name }} \ No newline at end of file +A domain manager was invited to {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/metadata_body.txt b/src/registrar/templates/emails/metadata_body.txt index a0a3682b7..adf0a186c 100644 --- a/src/registrar/templates/emails/metadata_body.txt +++ b/src/registrar/templates/emails/metadata_body.txt @@ -1 +1 @@ -{{ prefix }}An export of all .gov metadata. +An export of all .gov metadata. diff --git a/src/registrar/templates/emails/metadata_subject.txt b/src/registrar/templates/emails/metadata_subject.txt index c19b4c26e..5fdece7ef 100644 --- a/src/registrar/templates/emails/metadata_subject.txt +++ b/src/registrar/templates/emails/metadata_subject.txt @@ -1,2 +1,2 @@ -{{ prefix }}Domain metadata - {{current_date_str}} +Domain metadata - {{current_date_str}} diff --git a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt index ee5987512..3d6b2a140 100644 --- a/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_addition_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}An admin was invited to your .gov organization \ No newline at end of file +An admin was invited to your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt index 9a45a8bbc..e250b17f8 100644 --- a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt +++ b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt @@ -1 +1 @@ -{{ prefix }}An admin was removed from your .gov organization \ No newline at end of file +An admin was removed from your .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/portfolio_invitation_subject.txt b/src/registrar/templates/emails/portfolio_invitation_subject.txt index de9080196..552bb2bec 100644 --- a/src/registrar/templates/emails/portfolio_invitation_subject.txt +++ b/src/registrar/templates/emails/portfolio_invitation_subject.txt @@ -1 +1 @@ -{{ prefix }}You’ve been invited to a .gov organization \ No newline at end of file +You’ve been invited to a .gov organization \ No newline at end of file diff --git a/src/registrar/templates/emails/status_change_approved_subject.txt b/src/registrar/templates/emails/status_change_approved_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/status_change_approved_subject.txt +++ b/src/registrar/templates/emails/status_change_approved_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/status_change_subject.txt b/src/registrar/templates/emails/status_change_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/status_change_subject.txt +++ b/src/registrar/templates/emails/status_change_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/submission_confirmation_subject.txt b/src/registrar/templates/emails/submission_confirmation_subject.txt index cc146643a..51b2c745a 100644 --- a/src/registrar/templates/emails/submission_confirmation_subject.txt +++ b/src/registrar/templates/emails/submission_confirmation_subject.txt @@ -1 +1 @@ -{{ prefix }}Update on your .gov request: {{ domain_request.requested_domain.name }} +Update on your .gov request: {{ domain_request.requested_domain.name }} diff --git a/src/registrar/templates/emails/transition_domain_invitation_subject.txt b/src/registrar/templates/emails/transition_domain_invitation_subject.txt index b162341d9..526c7714b 100644 --- a/src/registrar/templates/emails/transition_domain_invitation_subject.txt +++ b/src/registrar/templates/emails/transition_domain_invitation_subject.txt @@ -1 +1 @@ -{{ prefix }}(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file +(Action required) Manage your .gov domain{% if domains|length > 1 %}s{% endif %} in the new registrar \ No newline at end of file diff --git a/src/registrar/templates/emails/update_to_approved_domain_subject.txt b/src/registrar/templates/emails/update_to_approved_domain_subject.txt index d952999a0..cf4c9a14c 100644 --- a/src/registrar/templates/emails/update_to_approved_domain_subject.txt +++ b/src/registrar/templates/emails/update_to_approved_domain_subject.txt @@ -1 +1 @@ -{{ prefix }}An update was made to {{domain}} \ No newline at end of file +An update was made to {{domain}} \ No newline at end of file diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 9323255af..b4caf42a5 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -60,13 +60,19 @@ def send_templated_email( # noqa # For email links ie getgov-rh.app.cloud.gov manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" + # Update the subject to have prefix here versus every email + subject_template = get_template(subject_template_name) + subject = subject_template.render(context=context) + subject = f"{prefix}{subject}" + # Adding to context context.update( { - "prefix": prefix, + "subject": subject, "manage_url": manage_url, } ) + # by default assume we can send to all addresses (prod has no whitelist) sendable_cc_addresses = cc_addresses @@ -89,9 +95,6 @@ def send_templated_email( # noqa if email_body: email_body.strip().lstrip("\n") - subject_template = get_template(subject_template_name) - subject = subject_template.render(context=context) - try: ses_client = boto3.client( "sesv2", From 33bf1988f1700ca149403555e3125697199c9128 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 08:28:50 -0800 Subject: [PATCH 41/50] Fix placement of where email subject is edited --- src/registrar/utility/email.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index b4caf42a5..39c7f21ac 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -60,18 +60,7 @@ def send_templated_email( # noqa # For email links ie getgov-rh.app.cloud.gov manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" - # Update the subject to have prefix here versus every email - subject_template = get_template(subject_template_name) - subject = subject_template.render(context=context) - subject = f"{prefix}{subject}" - - # Adding to context - context.update( - { - "subject": subject, - "manage_url": manage_url, - } - ) + context["manage_url"] = manage_url # by default assume we can send to all addresses (prod has no whitelist) sendable_cc_addresses = cc_addresses @@ -95,6 +84,13 @@ def send_templated_email( # noqa if email_body: email_body.strip().lstrip("\n") + # Update the subject to have prefix here versus every email + subject_template = get_template(subject_template_name) + subject = subject_template.render(context=context) + subject = f"{prefix}{subject}" + + context["subject"] = subject + try: ses_client = boto3.client( "sesv2", From c81c229be23e3883e445ecfaba3f7ff01b2ba347 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 14 Feb 2025 12:01:25 -0500 Subject: [PATCH 42/50] fixed test --- src/registrar/tests/test_email_invitations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index 20ac4a565..981bca6dd 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -919,6 +919,7 @@ class TestSendPortfolioMemberPermissionUpdateEmail(unittest.TestCase): "portfolio": permissions.portfolio, "requestor_email": "requestor@example.com", "permissions": permissions, + "date": date.today(), }, ) self.assertTrue(result) From 7d8249923e88a0fa4571d6290e5409d792bb42eb Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 09:17:28 -0800 Subject: [PATCH 43/50] Update comments --- src/registrar/utility/email.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 39c7f21ac..797ad4aa9 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -53,11 +53,13 @@ def send_templated_email( # noqa context = {} env_base_url = settings.BASE_URL - # The regular expresstion is to get both http (localhost) and https (everything else) + # The regular expression is to get both http (localhost) and https (everything else) env_name = re.sub(r"^https?://", "", env_base_url).split(".")[0] - # To add to subject lines ie [GETGOV-RH] + # If NOT in prod, add env to the subject line + # IE adds [GETGOV-RH] if we are in the -RH sandbox prefix = f"[{env_name.upper()}] " if not settings.IS_PRODUCTION else "" - # For email links ie getgov-rh.app.cloud.gov + # If NOT in prod, update instances of "manage.get.gov" links to point to + # current environment, ie "getgov-rh.app.cloud.gov" manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov" context["manage_url"] = manage_url From 026326b6eca501f9d00782c02ac7a947ac9338f6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 14 Feb 2025 11:15:14 -0800 Subject: [PATCH 44/50] Remove unneeded additional context udpate --- src/registrar/utility/email.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 797ad4aa9..94e87a96b 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -91,8 +91,6 @@ def send_templated_email( # noqa subject = subject_template.render(context=context) subject = f"{prefix}{subject}" - context["subject"] = subject - try: ses_client = boto3.client( "sesv2", From 16ed7546fd19db1963fc37bb0f260a690f06df9e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:35:00 -0700 Subject: [PATCH 45/50] Update analytics.js --- src/registrar/assets/src/js/getgov-admin/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index a5488fa3d..7524b726f 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -163,7 +163,7 @@ export function initAnalyticsDashboard() { ]; charts.forEach(chart => { if (chartInstances.has(chart.id)) chartInstances.get(chart.id).destroy(); - chartInstances.set(chart.id, createComparativeColumnChart(...chart, "Start Date", "End Date")); + chartInstances.set(chart.id, createComparativeColumnChart(chart.id, chart.title, "Start Date", "End Date")); }); // Add resize listener to each chart From 62626636aeba4a03ff7520ffd4b66143e7090a06 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 15 Feb 2025 08:16:17 -0500 Subject: [PATCH 46/50] Viewer, all => Viewer --- src/registrar/assets/src/js/getgov/portfolio-member-page.js | 2 +- src/registrar/forms/portfolio.py | 2 +- src/registrar/models/portfolio_invitation.py | 2 +- src/registrar/models/user_portfolio_permission.py | 2 +- src/registrar/models/utility/portfolio_helper.py | 4 ++-- src/registrar/tests/test_views_portfolio.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js index c96677ebc..95723fc7e 100644 --- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js +++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js @@ -128,7 +128,7 @@ export function initAddNewMemberPageListeners() { }); } else { // for admin users, the permissions are always the same - appendPermissionInContainer('Domains', 'Viewer, all', permissionDetailsContainer); + appendPermissionInContainer('Domains', 'Viewer', permissionDetailsContainer); appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer); appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer); } diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 3a9074b2d..9824ed68a 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -127,7 +127,7 @@ class BasePortfolioMemberForm(forms.ModelForm): domain_permissions = forms.ChoiceField( choices=[ (UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"), - (UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"), + (UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer"), ], widget=forms.RadioSelect, required=False, diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 045cf2de4..99febc92e 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -108,7 +108,7 @@ class PortfolioInvitation(TimeStampedModel): Returns a string representation of the user's domain access level. Uses the `get_domains_display` function to determine whether the user has - "Viewer, all" access (can view all domains) or "Viewer, limited" access. + "Viewer" access (can view all domains) or "Viewer, limited" access. Returns: str: The display name of the user's domain permissions. diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 202fc2e8d..e077daa57 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -204,7 +204,7 @@ class UserPortfolioPermission(TimeStampedModel): Returns a string representation of the user's domain access level. Uses the `get_domains_display` function to determine whether the user has - "Viewer, all" access (can view all domains) or "Viewer, limited" access. + "Viewer" access (can view all domains) or "Viewer, limited" access. Returns: str: The display name of the user's domain permissions. diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 00c5ce2b8..03733237e 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -105,7 +105,7 @@ def get_domains_display(roles, permissions): """ Determines the display name for a user's domain viewing permissions. - - If the user has the VIEW_ALL_DOMAINS permission, return "Viewer, all". + - If the user has the VIEW_ALL_DOMAINS permission, return "Viewer". - Otherwise, return "Viewer, limited". Args: @@ -118,7 +118,7 @@ def get_domains_display(roles, permissions): UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission") all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions) if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions: - return "Viewer, all" + return "Viewer" else: return "Viewer, limited" diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 65e0350ee..3ce1cfdfa 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1063,7 +1063,7 @@ class TestPortfolio(WebTest): self.assertContains(response, "Invited") self.assertContains(response, portfolio_invitation.email) self.assertContains(response, "Admin") - self.assertContains(response, "Viewer, all") + self.assertContains(response, "Viewer") self.assertContains(response, "Creator") self.assertContains(response, "Manager") self.assertContains( From 1b7871c4063233d8a8b8e6f5f13164edf5393eb4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Sun, 16 Feb 2025 18:56:20 -0700 Subject: [PATCH 47/50] stripped dead code --- src/registrar/models/user.py | 45 +-------------- src/registrar/templatetags/custom_filters.py | 7 --- src/registrar/tests/test_models.py | 61 -------------------- 3 files changed, 1 insertion(+), 112 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 82a0465c5..d5476ab9a 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -269,7 +269,7 @@ class User(AbstractUser): return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS) def is_portfolio_admin(self, portfolio): - return "Admin" in self.portfolio_role_summary(portfolio) + return self.has_edit_portfolio_permission(portfolio) def get_first_portfolio(self): permission = self.portfolio_permissions.first() @@ -277,49 +277,6 @@ class User(AbstractUser): return permission.portfolio return None - def portfolio_role_summary(self, portfolio): - """Returns a list of roles based on the user's permissions.""" - roles = [] - - # Define the conditions and their corresponding roles - conditions_roles = [ - (self.has_edit_portfolio_permission(portfolio), ["Admin"]), - ( - self.has_view_all_domains_portfolio_permission(portfolio) - and self.has_any_requests_portfolio_permission(portfolio) - and self.has_edit_request_portfolio_permission(portfolio), - ["View-only admin", "Domain requestor"], - ), - ( - self.has_view_all_domains_portfolio_permission(portfolio) - and self.has_any_requests_portfolio_permission(portfolio), - ["View-only admin"], - ), - ( - self.has_view_portfolio_permission(portfolio) - and self.has_edit_request_portfolio_permission(portfolio) - and self.has_any_domains_portfolio_permission(portfolio), - ["Domain requestor", "Domain manager"], - ), - ( - self.has_view_portfolio_permission(portfolio) and self.has_edit_request_portfolio_permission(portfolio), - ["Domain requestor"], - ), - ( - self.has_view_portfolio_permission(portfolio) and self.has_any_domains_portfolio_permission(portfolio), - ["Domain manager"], - ), - (self.has_view_portfolio_permission(portfolio), ["Member"]), - ] - - # Evaluate conditions and add roles - for condition, role_list in conditions_roles: - if condition: - roles.extend(role_list) - break - - return roles - def get_portfolios(self): return self.portfolio_permissions.all() diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index d21678d58..c0204b4ca 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -251,13 +251,6 @@ def is_members_subpage(path): return get_url_name(path) in url_names -@register.filter(name="portfolio_role_summary") -def portfolio_role_summary(user, portfolio): - """Returns the value of user.portfolio_role_summary""" - if user and portfolio: - return user.portfolio_role_summary(portfolio) - else: - return [] @register.filter(name="display_requesting_entity") diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 4401b73e8..cd2fe868d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1191,67 +1191,6 @@ class TestUser(TestCase): User.objects.all().delete() UserDomainRole.objects.all().delete() - @patch.object(User, "has_edit_portfolio_permission", return_value=True) - def test_portfolio_role_summary_admin(self, mock_edit_org): - # Test if the user is recognized as an Admin - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"]) - - @patch.multiple( - User, - has_view_all_domains_portfolio_permission=lambda self, portfolio: True, - has_any_requests_portfolio_permission=lambda self, portfolio: True, - has_edit_request_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self): - # Test if the user has both 'View-only admin' and 'Domain requestor' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["View-only admin", "Domain requestor"]) - - @patch.multiple( - User, - has_view_all_domains_portfolio_permission=lambda self, portfolio: True, - has_any_requests_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_view_only_admin(self): - # Test if the user is recognized as a View-only admin - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["View-only admin"]) - - @patch.multiple( - User, - has_view_portfolio_permission=lambda self, portfolio: True, - has_edit_request_portfolio_permission=lambda self, portfolio: True, - has_any_domains_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_member_domain_requestor_domain_manager(self): - # Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"]) - - @patch.multiple( - User, - has_view_portfolio_permission=lambda self, portfolio: True, - has_edit_request_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_member_domain_requestor(self): - # Test if the user has 'Member' and 'Domain requestor' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor"]) - - @patch.multiple( - User, - has_view_portfolio_permission=lambda self, portfolio: True, - has_any_domains_portfolio_permission=lambda self, portfolio: True, - ) - def test_portfolio_role_summary_member_domain_manager(self): - # Test if the user has 'Member' and 'Domain manager' roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain manager"]) - - @patch.multiple(User, has_view_portfolio_permission=lambda self, portfolio: True) - def test_portfolio_role_summary_member(self): - # Test if the user is recognized as a Member - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Member"]) - - def test_portfolio_role_summary_empty(self): - # Test if the user has no roles - self.assertEqual(self.user.portfolio_role_summary(self.portfolio), []) - @patch("registrar.models.User._has_portfolio_permission") def test_has_view_portfolio_permission(self, mock_has_permission): mock_has_permission.return_value = True From 70f5a655a8ae9a7b27aa778ecf68d9c1f401d815 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Sun, 16 Feb 2025 19:00:23 -0700 Subject: [PATCH 48/50] linted --- src/registrar/templatetags/custom_filters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index c0204b4ca..ff73e6dc1 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -251,8 +251,6 @@ def is_members_subpage(path): return get_url_name(path) in url_names - - @register.filter(name="display_requesting_entity") def display_requesting_entity(domain_request): """Workaround for a newline issue in .txt files (our emails) as if statements From 3493b9a16f9056ad4b625ae909f9768b29956557 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:30:30 -0700 Subject: [PATCH 49/50] Less specific selector --- src/registrar/assets/src/js/getgov-admin/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/src/js/getgov-admin/analytics.js b/src/registrar/assets/src/js/getgov-admin/analytics.js index 7524b726f..47bc81388 100644 --- a/src/registrar/assets/src/js/getgov-admin/analytics.js +++ b/src/registrar/assets/src/js/getgov-admin/analytics.js @@ -146,7 +146,7 @@ function createComparativeColumnChart(id, title, labelOne, labelTwo) { /** An IIFE to initialize the analytics page */ export function initAnalyticsDashboard() { - const analyticsPageContainer = document.querySelector('.analytics-dashboard .analytics-dashboard-charts'); + const analyticsPageContainer = document.querySelector('.analytics-dashboard-charts'); if (analyticsPageContainer) { document.addEventListener("DOMContentLoaded", function () { initAnalyticsExportButtons(); From 0f0048fe335582ba9bc8984db2b56980e8073158 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:34:50 -0700 Subject: [PATCH 50/50] fix button wrap --- src/registrar/templates/admin/analytics.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/analytics.html b/src/registrar/templates/admin/analytics.html index d345aeb14..fdebff22c 100644 --- a/src/registrar/templates/admin/analytics.html +++ b/src/registrar/templates/admin/analytics.html @@ -95,7 +95,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ -
    +