From a6b82a21bbb30c136e9b1687de1d47ff318dd8a4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 7 Nov 2024 09:39:29 -0700 Subject: [PATCH 001/148] domain request done page changes --- src/registrar/assets/sass/_theme/_base.scss | 4 ++++ src/registrar/templates/base.html | 2 ++ src/registrar/templates/domain_request_done.html | 10 ++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 85f453dac..056f219a1 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -29,6 +29,10 @@ body { padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15 } +#wrapper.wrapper--padding-top-4 { + padding-top: units(4); +} + #wrapper.dashboard { background-color: color('primary-lightest'); padding-top: units(5)!important; diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index b14dab2fa..3e2b9fc42 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -139,7 +139,9 @@ {% endblock header %} {% block wrapper %} + {% block wrapperdiv %}
+ {% endblock wrapperdiv %} {% block messages %} {% if messages %} -

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 homepage.

+

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.

Contact us if you need help during this process.

From 9b43b8df012c2b604dd88aecaed6493f6eac8303 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Fri, 8 Nov 2024 11:09:24 -0500 Subject: [PATCH 002/148] changes --- src/registrar/assets/sass/_theme/_base.scss | 4 ++++ .../portfolio_domain_request_additional_details.html | 2 +- src/registrar/templates/portfolio_requests.html | 4 ++-- src/registrar/views/domain_request.py | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 85f453dac..c4618555a 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -253,3 +253,7 @@ abbr[title] { .break-word { word-break: break-word; } + +.maxwidth-386{ + max-width: 386px !important; +} \ No newline at end of file diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html index 3c5b50d6b..08dc23c0b 100644 --- a/src/registrar/templates/portfolio_domain_request_additional_details.html +++ b/src/registrar/templates/portfolio_domain_request_additional_details.html @@ -13,7 +13,7 @@
-

Provide details below. *

+

This question is optional.

{% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% input_with_errors forms.0.anything_else %} {% endwith %} diff --git a/src/registrar/templates/portfolio_requests.html b/src/registrar/templates/portfolio_requests.html index d21bbcc4e..f762a42c8 100644 --- a/src/registrar/templates/portfolio_requests.html +++ b/src/registrar/templates/portfolio_requests.html @@ -14,12 +14,12 @@ {% endblock %}
-

Domain requests

+

Domain requests

{% if has_edit_request_portfolio_permission %}
-

Domain requests can only be modified by the person who created the request.

+

Domain requests can only be modified by the person who created the request.

{% comment %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index b1c5c9c89..2aed789e8 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -323,9 +323,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): request, "domain_request_intro.html", { - "hide_requests": True, - "hide_domains": True, - "hide_members": True, + "hide_requests": False, + "hide_domains": False, + "hide_members": False, }, ) else: From fa31b1eebe4d2ef001a8d84b3e3b20fb9b4218b3 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Fri, 8 Nov 2024 11:17:15 -0500 Subject: [PATCH 003/148] updates --- src/registrar/forms/domain_request_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index bfbc22124..43eb32d7f 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -774,7 +774,7 @@ class CisaRepresentativeYesNoForm(BaseYesNoForm): class AnythingElseForm(BaseDeletableRegistrarForm): anything_else = forms.CharField( - required=True, + required=False, label="Anything else?", widget=forms.Textarea(), validators=[ From 7d1b84d99f6fcf225f97bddaef843fc139a6bccb Mon Sep 17 00:00:00 2001 From: asaki222 Date: Fri, 8 Nov 2024 11:32:52 -0500 Subject: [PATCH 004/148] another update --- src/registrar/templates/portfolio_no_requests.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/portfolio_no_requests.html b/src/registrar/templates/portfolio_no_requests.html index c8eb3fe6e..a51a034a8 100644 --- a/src/registrar/templates/portfolio_no_requests.html +++ b/src/registrar/templates/portfolio_no_requests.html @@ -5,13 +5,13 @@ {% block title %} Domain Requests | {% endblock %} {% block portfolio_content %} -

Current domain requests

+

Domain requests

You don’t have access to domain requests.

{% if portfolio_administrators %} -

If you believe you should have access to a request, reach out to your organization’s administrators.

-

Your organizations administrators:

+

If you believe you should have access to requests, reach out to your organization’s administrators.

+

Your organization's administrators:

    {% for administrator in portfolio_administrators %} {% if administrator.email %} From d52ed725c1213807313670492ead7811d2d55f8b Mon Sep 17 00:00:00 2001 From: asaki222 Date: Fri, 8 Nov 2024 12:17:44 -0500 Subject: [PATCH 005/148] reverted additional details pages --- src/registrar/forms/domain_request_wizard.py | 2 +- .../templates/portfolio_domain_request_additional_details.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 43eb32d7f..bfbc22124 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -774,7 +774,7 @@ class CisaRepresentativeYesNoForm(BaseYesNoForm): class AnythingElseForm(BaseDeletableRegistrarForm): anything_else = forms.CharField( - required=False, + required=True, label="Anything else?", widget=forms.Textarea(), validators=[ diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html index 08dc23c0b..3c5b50d6b 100644 --- a/src/registrar/templates/portfolio_domain_request_additional_details.html +++ b/src/registrar/templates/portfolio_domain_request_additional_details.html @@ -13,7 +13,7 @@
    -

    This question is optional.

    +

    Provide details below. *

    {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% input_with_errors forms.0.anything_else %} {% endwith %} From 3311a8c9e23ca137e8cbdb73ff885bc2c01b2fe9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:24:29 -0700 Subject: [PATCH 006/148] remove hide request and domains from context --- src/registrar/views/domain_request.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 2aed789e8..b04b6e696 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -489,11 +489,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "user": self.request.user, "requested_domain__name": requested_domain_name, } - - # Hides the requests and domains buttons in the navbar - context_stuff["hide_requests"] = self.is_portfolio - context_stuff["hide_domains"] = self.is_portfolio - return context_stuff def get_step_list(self) -> list: From 6ead9a21d1d04231632e9abb078c62a87c7f05a5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:57:02 -0700 Subject: [PATCH 007/148] Add padding for withdraw view --- .../templates/domain_request_withdraw_confirmation.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/registrar/templates/domain_request_withdraw_confirmation.html b/src/registrar/templates/domain_request_withdraw_confirmation.html index edcdcadd3..03cb06b51 100644 --- a/src/registrar/templates/domain_request_withdraw_confirmation.html +++ b/src/registrar/templates/domain_request_withdraw_confirmation.html @@ -3,6 +3,10 @@ {% block title %}Withdraw request for {{ DomainRequest.requested_domain.name }} | {% endblock %} {% load static url_helpers %} +{% block wrapperdiv %} +
    +{% endblock wrapperdiv %} + {% block content %}
    From 14eaf29e8a2eba0b6530737b1aeb15ac823296ef Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:02:38 -0700 Subject: [PATCH 008/148] Add Wide screen mode --- src/registrar/context_processors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 53f6e8ae7..7ae07e0fc 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -97,5 +97,5 @@ def portfolio_permissions(request): def is_widescreen_mode(request): - widescreen_paths = ["/domains/", "/requests/", "/members/"] + widescreen_paths = ["/domains/", "/requests/", "/members/", "/request/"] return {"is_widescreen_mode": any(path in request.path for path in widescreen_paths) or request.path == "/"} From e9b858814c4933c26e7cd3c010949183df961eb2 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Fri, 8 Nov 2024 13:31:39 -0500 Subject: [PATCH 009/148] update usa-current --- src/registrar/tests/test_views_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 73e538df3..72baa0cf8 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -3152,7 +3152,7 @@ class TestDomainRequestWizard(TestWithUser, WebTest): self.assertContains(detail_page, "#lock", 1) # The current option should be selected - self.assertContains(detail_page, "usa-current", count=1) + self.assertContains(detail_page, "usa-current", count=2) # We default to the requesting entity page expected_url = reverse("domain-request:portfolio_requesting_entity") From fa7fd601636038b585bf195cd5092a18bc7d1f04 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Fri, 8 Nov 2024 13:52:07 -0500 Subject: [PATCH 010/148] removed check since the domain and domain requests are in the navigation bar --- src/registrar/tests/test_views_request.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 72baa0cf8..9a847b6d5 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -3158,11 +3158,6 @@ class TestDomainRequestWizard(TestWithUser, WebTest): expected_url = reverse("domain-request:portfolio_requesting_entity") # This returns the entire url, thus "in" self.assertIn(expected_url, detail_page.request.url) - - # We shouldn't show the "domains" and "domain requests" buttons - # on this page. - self.assertNotContains(detail_page, "Domains") - self.assertNotContains(detail_page, "Domain requests") else: self.fail(f"Expected a redirect, but got a different response: {response}") From 89c0f86cc0184e0417d9137b3fe48b38eb6e3f77 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 11 Nov 2024 18:10:42 -0500 Subject: [PATCH 011/148] test --- src/registrar/tests/test_views_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 9a847b6d5..aec988b81 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -3162,6 +3162,7 @@ class TestDomainRequestWizard(TestWithUser, WebTest): self.fail(f"Expected a redirect, but got a different response: {response}") # Data cleanup + # test deployment user_portfolio_permission.delete() portfolio.delete() federal_agency.delete() From e1f6e2112cde3153060fb1b3256cefb00776ede1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:11:12 -0700 Subject: [PATCH 012/148] Add widescreen to no-org, and added portfolio specific page rule --- src/registrar/context_processors.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 7ae07e0fc..015772b9f 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -97,5 +97,18 @@ def portfolio_permissions(request): def is_widescreen_mode(request): - widescreen_paths = ["/domains/", "/requests/", "/members/", "/request/"] - return {"is_widescreen_mode": any(path in request.path for path in widescreen_paths) or request.path == "/"} + widescreen_paths = [] + portfolio_widescreen_paths = [ + "/domains/", + "/requests/", + "/request/", + "/no-organization-requests/", + "/no-organization-domains/", + "/domain-request/", + ] + is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/" + is_portfolio_widescreen = bool( + request.user.is_org_user(request) and + any(path in request.path for path in portfolio_widescreen_paths) + ) + return {"is_widescreen_mode": is_widescreen or is_portfolio_widescreen} From b0dc18cc3ff4670e13f5fa18f6fdd3a15b6e1db2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:32:07 -0700 Subject: [PATCH 013/148] base --- src/registrar/utility/csv_export.py | 177 ++++++++++++++++++---------- 1 file changed, 113 insertions(+), 64 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 64d960337..e90b27c29 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -15,6 +15,7 @@ from django.db.models import Case, CharField, Count, DateField, F, ManyToManyFie from django.utils import timezone from django.db.models.functions import Concat, Coalesce from django.contrib.postgres.aggregates import StringAgg +from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.templatetags.custom_filters import get_region from registrar.utility.constants import BranchChoices @@ -50,11 +51,7 @@ def format_end_date(end_date): return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date() -class BaseExport(ABC): - """ - A generic class for exporting data which returns a csv file for the given model. - Base class in an inheritance tree of 3. - """ +class BaseModelDict(ABC): @classmethod @abstractmethod @@ -65,13 +62,6 @@ class BaseExport(ABC): """ pass - @classmethod - def get_columns(cls): - """ - Returns the columns for CSV export. Override in subclasses as needed. - """ - return [] - @classmethod def get_sort_fields(cls): """ @@ -116,7 +106,7 @@ class BaseExport(ABC): return Q() @classmethod - def get_computed_fields(cls): + def get_annotated_fields(cls): """ Get a dict of computed fields. These are fields that do not exist on the model normally and will be passed to .annotate() when building a queryset. @@ -136,25 +126,10 @@ class BaseExport(ABC): Get a list of fields from related tables. """ return [] - - @classmethod - def update_queryset(cls, queryset, **kwargs): - """ - Returns an updated queryset. Override in subclass to update queryset. - """ - return queryset - - @classmethod - def write_csv_before(cls, csv_writer, **export_kwargs): - """ - Write to csv file before the write_csv method. - Override in subclasses where needed. - """ - pass - + @classmethod def annotate_and_retrieve_fields( - cls, initial_queryset, computed_fields, related_table_fields=None, include_many_to_many=False, **kwargs + cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs ) -> QuerySet: """ Applies annotations to a queryset and retrieves specified fields, @@ -162,7 +137,7 @@ class BaseExport(ABC): Parameters: initial_queryset (QuerySet): Initial queryset. - computed_fields (dict, optional): Fields to compute {field_name: expression}. + annotated_fields (dict, optional): Fields to compute {field_name: expression}. related_table_fields (list, optional): Extra fields to retrieve; defaults to annotation keys if None. include_many_to_many (bool, optional): Determines if we should include many to many fields or not **kwargs: Additional keyword arguments for specific parameters (e.g., public_contacts, domain_invitations, @@ -176,8 +151,8 @@ class BaseExport(ABC): # We can infer that if we're passing in annotations, # we want to grab the result of said annotation. - if computed_fields: - related_table_fields.extend(computed_fields.keys()) + if annotated_fields: + related_table_fields.extend(annotated_fields.keys()) # Get prexisting fields on the model model_fields = set() @@ -187,10 +162,109 @@ class BaseExport(ABC): if many_to_many or not isinstance(field, ManyToManyField): model_fields.add(field.name) - queryset = initial_queryset.annotate(**computed_fields).values(*model_fields, *related_table_fields) + queryset = initial_queryset.annotate(**annotated_fields).values(*model_fields, *related_table_fields) return cls.update_queryset(queryset, **kwargs) + @classmethod + def update_queryset(cls, queryset, **kwargs): + """ + Returns an updated queryset. Override in subclass to update queryset. + """ + return queryset + + @classmethod + def get_model_dict(cls): + sort_fields = cls.get_sort_fields() + kwargs = cls.get_additional_args() + select_related = cls.get_select_related() + prefetch_related = cls.get_prefetch_related() + exclusions = cls.get_exclusions() + annotations_for_sort = cls.get_annotations_for_sort() + filter_conditions = cls.get_filter_conditions(**kwargs) + annotated_fields = cls.get_annotated_fields() + related_table_fields = cls.get_related_table_fields() + + model_queryset = ( + cls.model() + .objects + .select_related(*select_related) + .prefetch_related(*prefetch_related) + .filter(filter_conditions) + .exclude(exclusions) + .annotate(**annotations_for_sort) + .order_by(*sort_fields) + .distinct() + ) + annotated_queryset = cls.annotate_and_retrieve_fields( + model_queryset, annotated_fields, related_table_fields, **kwargs + ) + models_dict = convert_queryset_to_dict(annotated_queryset, is_model=False) + + return models_dict + + +class UserPortfolioPermissionModelDict(BaseModelDict): + + @classmethod + def model(cls): + # Return the model class that this export handles + return UserPortfolioPermission + + @classmethod + def get_filter_conditions(cls, **export_kwargs): + """ + Get a Q object of filter conditions to filter when building queryset. + """ + return Q() + + @classmethod + def get_exclusions(cls): + """ + Get a Q object of exclusion conditions to pass to .exclude() when building queryset. + """ + return Q() + + @classmethod + def get_annotated_fields(cls): + """ + Get a dict of computed fields. These are fields that do not exist on the model normally + and will be passed to .annotate() when building a queryset. + """ + return {} + + @classmethod + def parse_row(cls, columns, model): + """ + Given a set of columns and a model dictionary, generate a new row from cleaned column data. + """ + FIELDS = {"Not yet defined": "Not yet defined"} + + row = [FIELDS.get(column, "") for column in columns] + return row + + +class BaseExport(BaseModelDict): + """ + A generic class for exporting data which returns a csv file for the given model. + Base class in an inheritance tree of 3. + """ + + @classmethod + def get_columns(cls): + """ + Returns the columns for CSV export. Override in subclasses as needed. + """ + return [] + + @classmethod + def write_csv_before(cls, csv_writer, **export_kwargs): + """ + Write to csv file before the write_csv method. + Override in subclasses where needed. + """ + pass + @classmethod def export_data_to_csv(cls, csv_file, **export_kwargs): """ @@ -199,32 +273,7 @@ class BaseExport(ABC): """ writer = csv.writer(csv_file) columns = cls.get_columns() - sort_fields = cls.get_sort_fields() - kwargs = cls.get_additional_args() - select_related = cls.get_select_related() - prefetch_related = cls.get_prefetch_related() - exclusions = cls.get_exclusions() - annotations_for_sort = cls.get_annotations_for_sort() - filter_conditions = cls.get_filter_conditions(**export_kwargs) - computed_fields = cls.get_computed_fields() - related_table_fields = cls.get_related_table_fields() - - model_queryset = ( - cls.model() - .objects.select_related(*select_related) - .prefetch_related(*prefetch_related) - .filter(filter_conditions) - .exclude(exclusions) - .annotate(**annotations_for_sort) - .order_by(*sort_fields) - .distinct() - ) - - # Convert the queryset to a dictionary (including annotated fields) - annotated_queryset = cls.annotate_and_retrieve_fields( - model_queryset, computed_fields, related_table_fields, **kwargs - ) - models_dict = convert_queryset_to_dict(annotated_queryset, is_model=False) + models_dict = cls.get_model_dict() # Write to csv file before the write_csv cls.write_csv_before(writer, **export_kwargs) @@ -534,7 +583,7 @@ class DomainDataType(DomainExport): return ["permissions"] @classmethod - def get_computed_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", "): """ Get a dict of computed fields. """ @@ -751,7 +800,7 @@ class DomainDataFull(DomainExport): ) @classmethod - def get_computed_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", "): """ Get a dict of computed fields. """ @@ -846,7 +895,7 @@ class DomainDataFederal(DomainExport): ) @classmethod - def get_computed_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", "): """ Get a dict of computed fields. """ @@ -1465,7 +1514,7 @@ class DomainRequestDataFull(DomainRequestExport): ] @classmethod - def get_computed_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", "): """ Get a dict of computed fields. """ From ed3d5bd39247e5f65eeef32fe4b29c5d5b7abd43 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:35:00 -0700 Subject: [PATCH 014/148] readd context fix --- src/registrar/context_processors.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 7ae07e0fc..32022aa34 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -96,6 +96,20 @@ def portfolio_permissions(request): return portfolio_context + def is_widescreen_mode(request): - widescreen_paths = ["/domains/", "/requests/", "/members/", "/request/"] - return {"is_widescreen_mode": any(path in request.path for path in widescreen_paths) or request.path == "/"} + widescreen_paths = [] + portfolio_widescreen_paths = [ + "/domains/", + "/requests/", + "/request/", + "/no-organization-requests/", + "/no-organization-domains/", + "/domain-request/", + ] + is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/" + is_portfolio_widescreen = bool( + request.user.is_org_user(request) and + any(path in request.path for path in portfolio_widescreen_paths) + ) + return {"is_widescreen_mode": is_widescreen or is_portfolio_widescreen} From 9be154261b1d6eacdfae9bc76a8ac27320d87d32 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:57:21 -0700 Subject: [PATCH 015/148] Add padding top 6 --- src/registrar/assets/sass/_theme/_base.scss | 4 ++-- src/registrar/templates/domain_request_done.html | 2 +- .../templates/domain_request_withdraw_confirmation.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 1b8f34290..442c5c862 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -29,8 +29,8 @@ body { padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15 } -#wrapper.wrapper--padding-top-4 { - padding-top: units(4); +#wrapper.wrapper--padding-top-6 { + padding-top: units(6); } #wrapper.dashboard { diff --git a/src/registrar/templates/domain_request_done.html b/src/registrar/templates/domain_request_done.html index 50cdc1634..0d38309d8 100644 --- a/src/registrar/templates/domain_request_done.html +++ b/src/registrar/templates/domain_request_done.html @@ -7,7 +7,7 @@ {% comment %} Same as the old wrapper implementation but with padding-top-4 {% endcomment %} {% block wrapperdiv %} -
    +
    {% endblock wrapperdiv %} {% block content %} diff --git a/src/registrar/templates/domain_request_withdraw_confirmation.html b/src/registrar/templates/domain_request_withdraw_confirmation.html index 03cb06b51..e1a5f0c2a 100644 --- a/src/registrar/templates/domain_request_withdraw_confirmation.html +++ b/src/registrar/templates/domain_request_withdraw_confirmation.html @@ -4,7 +4,7 @@ {% load static url_helpers %} {% block wrapperdiv %} -
    +
    {% endblock wrapperdiv %} {% block content %} From 7089977c16f2a5ede439e09cd6f6f9b2695a9cb1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:19:44 -0700 Subject: [PATCH 016/148] add portfolio name --- src/registrar/templates/domain_request_requesting_entity.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index d09e8ab89..3ce792531 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -2,7 +2,7 @@ {% load field_helpers url_helpers %} {% block form_instructions %} -

    To help with our review, we need to understand whether the domain you're requesting will be used by the Department of Energy or by one of its suborganizations.

    +

    To help with our review, we need to understand whether the domain you're requesting will be used by {{ portfolio }} or by one of its suborganizations.

    We define a suborganization as any entity (agency, bureau, office) that falls under the overarching organization.

    {% endblock %} From 425e7e995b9198beddc6e58009019ea34bccfee3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:20:55 -0700 Subject: [PATCH 017/148] fix test --- src/registrar/context_processors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 32022aa34..10a4fcb89 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -109,6 +109,7 @@ def is_widescreen_mode(request): ] is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/" is_portfolio_widescreen = bool( + hasattr(request.user, "is_org_user") and request.user.is_org_user(request) and any(path in request.path for path in portfolio_widescreen_paths) ) From 23702ab0bb0aef9ad9d1ec61366a616772c28f2f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:29:42 -0700 Subject: [PATCH 018/148] fix test and lint --- src/registrar/context_processors.py | 7 +++---- src/registrar/tests/test_views_portfolio.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 10a4fcb89..ae35a8865 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -96,7 +96,6 @@ def portfolio_permissions(request): return portfolio_context - def is_widescreen_mode(request): widescreen_paths = [] portfolio_widescreen_paths = [ @@ -109,8 +108,8 @@ def is_widescreen_mode(request): ] is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/" is_portfolio_widescreen = bool( - hasattr(request.user, "is_org_user") and - request.user.is_org_user(request) and - any(path in request.path for path in portfolio_widescreen_paths) + hasattr(request.user, "is_org_user") + and request.user.is_org_user(request) + and any(path in request.path for path in portfolio_widescreen_paths) ) return {"is_widescreen_mode": is_widescreen or is_portfolio_widescreen} diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 402d23b70..775cb04e6 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1658,7 +1658,7 @@ class TestRequestingEntity(WebTest): self.assertContains(response, "Add suborganization information") # We expect to see the portfolio name in two places: # the header, and as one of the radio button options. - self.assertContains(response, self.portfolio.organization_name, count=2) + self.assertContains(response, self.portfolio.organization_name, count=3) # We expect the dropdown list to contain the suborganizations that currently exist on this portfolio self.assertContains(response, self.suborganization.name, count=1) From 13faf5bba558e4491faea95e6c5365f8da6ccafe Mon Sep 17 00:00:00 2001 From: asaki222 Date: Tue, 12 Nov 2024 19:37:43 -0500 Subject: [PATCH 019/148] changes --- src/registrar/assets/js/get-gov.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index adcc21d2a..3d6a54221 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1814,10 +1814,10 @@ class DomainRequestsTable extends LoadTableBase { ${submissionDate} ${markupCreatorRow} - + ${request.status} - +

    Add suborganization information

    This information will be published in .gov’s public data. If you don’t see your suborganization in the list, - select “other” and enter the name or your suborganization. + select “other”.

    {% with attr_required=True %} {% input_with_errors forms.1.sub_organization %} From e4e1d71526960dab94b1274cd2e128155709b857 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:44:14 -0700 Subject: [PATCH 021/148] Fix padding on suborg, and on portfolio tables --- src/registrar/assets/js/get-gov.js | 11 ++++++++++- src/registrar/assets/sass/_theme/_base.scss | 4 ---- src/registrar/templates/portfolio_base.html | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e317a6647..f7cc9d540 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2786,9 +2786,10 @@ document.addEventListener('DOMContentLoaded', function() { const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`); const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`); const select = document.getElementById(`id_${formPrefix}-sub_organization`); + const selectParent = select?.parentElement; const suborgContainer = document.getElementById("suborganization-container"); const suborgDetailsContainer = document.getElementById("suborganization-container__details"); - if (!radios || !select || !suborgContainer || !suborgDetailsContainer) return; + if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return; // requestingSuborganization: This just broadly determines if they're requesting a suborg at all // requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not. @@ -2799,6 +2800,14 @@ document.addEventListener('DOMContentLoaded', function() { if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True"; requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer); requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False"; + + if (requestingNewSuborganization.value === "True") { + selectParent.classList.add("padding-bottom-2"); + showElement(suborgDetailsContainer); + }else { + selectParent.classList.remove("padding-bottom-2"); + hideElement(suborgDetailsContainer); + } requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer); } diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 442c5c862..891f950e6 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -38,10 +38,6 @@ body { padding-top: units(5)!important; } -#wrapper.dashboard--portfolio { - padding-top: units(4)!important; -} - #wrapper.dashboard--grey-1 { background-color: color('gray-1'); } diff --git a/src/registrar/templates/portfolio_base.html b/src/registrar/templates/portfolio_base.html index 86e43c962..1963d7cca 100644 --- a/src/registrar/templates/portfolio_base.html +++ b/src/registrar/templates/portfolio_base.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block wrapper %} -
    +
    {% block content %}
    From 192e4999536040027086f446d2305bfcc2ea0705 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:00:07 -0700 Subject: [PATCH 022/148] lint + test --- src/registrar/forms/domain_request_wizard.py | 5 ++++- src/registrar/tests/test_views_portfolio.py | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 356048734..4ac121641 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -119,7 +119,10 @@ class RequestingEntityForm(RegistrarForm): if not cleaned_data.get("suborganization_city"): self.add_error("suborganization_city", "Enter the city where your suborganization is located.") if not cleaned_data.get("suborganization_state_territory"): - self.add_error("suborganization_state_territory", "Select the state, territory, or military post where your suborganization is located") + self.add_error( + "suborganization_state_territory", + "Select the state, territory, or military post where your suborganization is located", + ) elif not suborganization: self.add_error("sub_organization", "Suborganization is required.") diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 775cb04e6..9c27dcb15 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1794,9 +1794,13 @@ class TestRequestingEntity(WebTest): form["portfolio_requesting_entity-is_requesting_new_suborganization"] = True response = form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.assertContains(response, "Requested suborganization is required.", status_code=200) - self.assertContains(response, "City is required.", status_code=200) - self.assertContains(response, "State, territory, or military post is required.", status_code=200) + self.assertContains(response, "Enter the name of your suborganization.", status_code=200) + self.assertContains(response, "Enter the city where your suborganization is located.", status_code=200) + self.assertContains( + response, + "Select the state, territory, or military post where your suborganization is located", + status_code=200, + ) @override_flag("organization_feature", active=True) @override_flag("organization_requests", active=True) From 792184967c5da2aa4ed6e7c3f2673bcb4bb1867a Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 13 Nov 2024 12:15:06 -0500 Subject: [PATCH 023/148] added period --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/forms/domain_request_wizard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e317a6647..5fe824b2b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1817,7 +1817,7 @@ class DomainRequestsTable extends LoadTableBase { ${request.status} - +

    Add suborganization information

    - This information will be published in .gov’s public data. If you don’t see your suborganization in the list, - select “other”. + This information will be published in .gov’s public data. If you don’t see your suborganization in the list, + select “other.”

    {% with attr_required=True %} {% input_with_errors forms.1.sub_organization %} From 973df828fa264ebcb8162c8f1725d6745341023b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:55:04 -0700 Subject: [PATCH 025/148] Possessive --- src/registrar/templates/portfolio_no_domains.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/portfolio_no_domains.html b/src/registrar/templates/portfolio_no_domains.html index 75ff3a91f..ac6a8c036 100644 --- a/src/registrar/templates/portfolio_no_domains.html +++ b/src/registrar/templates/portfolio_no_domains.html @@ -18,7 +18,7 @@

    You aren’t managing any domains.

    {% if portfolio_administrators %}

    If you believe you should have access to a domain, reach out to your organization’s administrators.

    -

    Your organizations administrators:

    +

    Your organization's administrators:

      {% for administrator in portfolio_administrators %} {% if administrator.email %} From c7fa22a64f458007a09519af2b320fb62eb02255 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:55:39 -0700 Subject: [PATCH 026/148] Fix test (again) --- src/registrar/tests/test_views_portfolio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 9c27dcb15..59c7a6f33 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1798,7 +1798,7 @@ class TestRequestingEntity(WebTest): self.assertContains(response, "Enter the city where your suborganization is located.", status_code=200) self.assertContains( response, - "Select the state, territory, or military post where your suborganization is located", + "Select the state, territory, or military post where your suborganization is located.", status_code=200, ) From 2265b70b5043c0efa6100f9ce4452f54427f9a30 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:10:11 -0700 Subject: [PATCH 027/148] Refactor part 1 --- src/registrar/models/utility/orm_helper.py | 6 + src/registrar/utility/csv_export.py | 242 +++++++++++++++--- src/registrar/views/portfolio_members_json.py | 121 ++------- 3 files changed, 239 insertions(+), 130 deletions(-) create mode 100644 src/registrar/models/utility/orm_helper.py diff --git a/src/registrar/models/utility/orm_helper.py b/src/registrar/models/utility/orm_helper.py new file mode 100644 index 000000000..24f7982e7 --- /dev/null +++ b/src/registrar/models/utility/orm_helper.py @@ -0,0 +1,6 @@ +from django.db.models.expressions import Func + +class ArrayRemove(Func): + """Custom Func to use array_remove to remove null values""" + function = "array_remove" + template = "%(function)s(%(expressions)s, NULL)" \ No newline at end of file diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index e90b27c29..d39ab996c 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -10,16 +10,20 @@ from registrar.models import ( DomainInformation, PublicContact, UserDomainRole, + PortfolioInvitation, ) -from django.db.models import Case, CharField, Count, DateField, F, ManyToManyField, Q, QuerySet, Value, When +from django.db.models import Case, CharField, Count, DateField, F, ManyToManyField, Q, QuerySet, Value, When, TextField, OuterRef, Subquery +from django.db.models.functions import Cast from django.utils import timezone from django.db.models.functions import Concat, Coalesce from django.contrib.postgres.aggregates import StringAgg from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict +from registrar.models.utility.orm_helper import ArrayRemove from registrar.templatetags.custom_filters import get_region from registrar.utility.constants import BranchChoices from registrar.utility.enums import DefaultEmail +from django.contrib.postgres.aggregates import ArrayAgg logger = logging.getLogger(__name__) @@ -167,14 +171,7 @@ class BaseModelDict(ABC): return cls.update_queryset(queryset, **kwargs) @classmethod - def update_queryset(cls, queryset, **kwargs): - """ - Returns an updated queryset. Override in subclass to update queryset. - """ - return queryset - - @classmethod - def get_model_dict(cls): + def get_annotated_queryset(cls, request=None): sort_fields = cls.get_sort_fields() kwargs = cls.get_additional_args() select_related = cls.get_select_related() @@ -196,12 +193,21 @@ class BaseModelDict(ABC): .order_by(*sort_fields) .distinct() ) - annotated_queryset = cls.annotate_and_retrieve_fields( + + return cls.annotate_and_retrieve_fields( model_queryset, annotated_fields, related_table_fields, **kwargs ) - models_dict = convert_queryset_to_dict(annotated_queryset, is_model=False) - return models_dict + @classmethod + def update_queryset(cls, queryset, **kwargs): + """ + Returns an updated queryset. Override in subclass to update queryset. + """ + return queryset + + @classmethod + def get_models_dict(cls, request=None): + return convert_queryset_to_dict(cls.get_annotated_queryset(request), is_model=False) class UserPortfolioPermissionModelDict(BaseModelDict): @@ -212,36 +218,170 @@ class UserPortfolioPermissionModelDict(BaseModelDict): return UserPortfolioPermission @classmethod - def get_filter_conditions(cls, **export_kwargs): + def get_select_related(cls): + """ + Get a list of tables to pass to select_related when building queryset. + """ + return ["user"] + + @classmethod + def get_filter_conditions(cls, portfolio): """ Get a Q object of filter conditions to filter when building queryset. """ - return Q() + if not portfolio: + # Return nothing + return Q(id__in=[]) + + # Get all members on this portfolio + return Q(portfolio=portfolio) @classmethod - def get_exclusions(cls): - """ - Get a Q object of exclusion conditions to pass to .exclude() when building queryset. - """ - return Q() - - @classmethod - def get_annotated_fields(cls): + def get_annotated_fields(cls, portfolio): """ Get a dict of computed fields. These are fields that do not exist on the model normally and will be passed to .annotate() when building a queryset. """ - return {} + if not portfolio: + # Return nothing + return {} + + return { + "first_name": F("user__first_name"), + "last_name": F("user__last_name"), + "email_display": F("user__email"), + "last_active": Coalesce( + Cast(F("user__last_login"), output_field=TextField()), + Value("Invalid date"), + output_field=TextField(), + ), + "additional_permissions_display": F("additional_permissions"), + "member_display": Case( + When( + Q(user__email__isnull=False) & ~Q(user__email=""), + then=F("user__email") + ), + When( + Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), + then=Concat( + Coalesce(F("user__first_name"), Value("")), + Value(" "), + Coalesce(F("user__last_name"), Value("")), + ), + ), + default=Value(""), + output_field=CharField(), + ), + "domain_info": ArrayAgg( + Concat( + F("user__permissions__domain_id"), + Value(":"), + F("user__permissions__domain__name"), + output_field=CharField(), + ), + distinct=True, + filter=Q(user__permissions__domain__isnull=False) + & Q(user__permissions__domain__domain_info__portfolio=portfolio), + ), + "source": Value("permission", output_field=CharField()), + } + + @classmethod + def get_annotated_queryset(cls, portfolio): + """Override of the base annotated queryset to pass in portfolio""" + model_queryset = ( + cls.model() + .objects + .select_related(*cls.get_select_related()) + .prefetch_related(*cls.get_prefetch_related()) + .filter(cls.get_filter_conditions(portfolio)) + .exclude(cls.get_exclusions()) + .annotate(**cls.get_annotations_for_sort()) + .order_by(*cls.get_sort_fields()) + .distinct() + ) + + annotated_fields = cls.get_annotated_fields(portfolio) + related_table_fields = cls.get_related_table_fields() + return cls.annotate_and_retrieve_fields( + model_queryset, annotated_fields, related_table_fields + ) + + +class PortfolioInvitationModelDict(BaseModelDict): @classmethod - def parse_row(cls, columns, model): - """ - Given a set of columns and a model dictionary, generate a new row from cleaned column data. - """ - FIELDS = {"Not yet defined": "Not yet defined"} + def model(cls): + # Return the model class that this export handles + return PortfolioInvitation - row = [FIELDS.get(column, "") for column in columns] - return row + @classmethod + def get_filter_conditions(cls, portfolio): + """ + Get a Q object of filter conditions to filter when building queryset. + """ + if not portfolio: + # Return nothing + return Q(id__in=[]) + + # Get all members on this portfolio + return Q( + # Check if email matches the OuterRef("email") + email=OuterRef("email"), + # Check if the domain's portfolio matches the given portfolio) + domain__domain_info__portfolio=portfolio, + ) + + @classmethod + def get_annotated_fields(cls, portfolio): + """ + Get a dict of computed fields. These are fields that do not exist on the model normally + and will be passed to .annotate() when building a queryset. + """ + if not portfolio: + # Return nothing + return {} + + domain_invitations = DomainInvitation.objects.filter( + email=OuterRef("email"), # Check if email matches the OuterRef("email") + domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio + ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) + return { + "first_name": Value(None, output_field=CharField()), + "last_name": Value(None, output_field=CharField()), + "email_display": F("email"), + "last_active": Value("Invited", output_field=TextField()), + "additional_permissions_display": F("additional_permissions"), + "member_display": F("email"), + "domain_info": ArrayRemove( + ArrayAgg( + Subquery(domain_invitations.values("domain_info")), + distinct=True, + ) + ), + "source": Value("invitation", output_field=CharField()), + } + + @classmethod + def get_annotated_queryset(cls, portfolio): + """Override of the base annotated queryset to pass in portfolio""" + model_queryset = ( + cls.model() + .objects + .select_related(*cls.get_select_related()) + .prefetch_related(*cls.get_prefetch_related()) + .filter(cls.get_filter_conditions(portfolio)) + .exclude(cls.get_exclusions()) + .annotate(**cls.get_annotations_for_sort()) + .order_by(*cls.get_sort_fields()) + .distinct() + ) + + annotated_fields = cls.get_annotated_fields(portfolio) + related_table_fields = cls.get_related_table_fields() + return cls.annotate_and_retrieve_fields( + model_queryset, annotated_fields, related_table_fields + ) class BaseExport(BaseModelDict): @@ -266,14 +406,14 @@ class BaseExport(BaseModelDict): pass @classmethod - def export_data_to_csv(cls, csv_file, **export_kwargs): + def export_data_to_csv(cls, csv_file, request=None, **export_kwargs): """ All domain metadata: Exports domains of all statuses plus domain managers. """ writer = csv.writer(csv_file) columns = cls.get_columns() - models_dict = cls.get_model_dict() + models_dict = cls.get_models_dict() # Write to csv file before the write_csv cls.write_csv_before(writer, **export_kwargs) @@ -321,6 +461,43 @@ class BaseExport(BaseModelDict): """ pass +class MemberExport(BaseExport): + + @classmethod + def model(self): + """ + No model is defined for the member report as it is a combination of multiple fields. + This is a special edge case, but the base report requires this to be defined. + """ + return None + + @classmethod + def get_models_dict(cls, request=None): + portfolio = request.session.get("portfolio") + if not portfolio: + return {} + + # Union the two querysets to combine UserPortfolioPermission + invites + permissions = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio) + invitations = PortfolioInvitationModelDict.get_annotated_queryset(portfolio) + objects = permissions.union(invitations) + return convert_queryset_to_dict(objects, is_model=False) + + @classmethod + def get_columns(cls): + """ + Returns the columns for CSV export. Override in subclasses as needed. + """ + return [] + + @classmethod + @abstractmethod + def parse_row(cls, columns, model): + """ + Given a set of columns and a model dictionary, generate a new row from cleaned column data. + Must be implemented by subclasses + """ + pass class DomainExport(BaseExport): """ @@ -1597,3 +1774,4 @@ class DomainRequestDataFull(DomainRequestExport): distinct=True, ) return query + diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 17209f0d9..408d37ff5 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -7,10 +7,9 @@ from django.contrib.postgres.aggregates import ArrayAgg from django.urls import reverse from django.views import View -from registrar.models.domain_invitation import DomainInvitation -from registrar.models.portfolio_invitation import PortfolioInvitation -from registrar.models.user_portfolio_permission import UserPortfolioPermission +from registrar.models import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices +from registrar.utility.csv_export import UserPortfolioPermissionModelDict, PortfolioInvitationModelDict from registrar.views.utility.mixins import PortfolioMembersPermission @@ -39,7 +38,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): page_number = request.GET.get("page", 1) page_obj = paginator.get_page(page_number) - members = [self.serialize_members(request, portfolio, item, request.user) for item in page_obj.object_list] + members = [self.serialize_members(portfolio, item, request.user) for item in page_obj.object_list] return JsonResponse( { @@ -56,92 +55,25 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): def initial_permissions_search(self, portfolio): """Perform initial search for permissions before applying any filters.""" - permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) - permissions = ( - permissions.select_related("user") - .annotate( - first_name=F("user__first_name"), - last_name=F("user__last_name"), - email_display=F("user__email"), - last_active=Coalesce( - Cast(F("user__last_login"), output_field=TextField()), # Cast last_login to text - Value("Invalid date"), - output_field=TextField(), - ), - additional_permissions_display=F("additional_permissions"), - member_display=Case( - # If email is present and not blank, use email - When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), - # If first name or last name is present, use concatenation of first_name + " " + last_name - When( - Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), - then=Concat( - Coalesce(F("user__first_name"), Value("")), - Value(" "), - Coalesce(F("user__last_name"), Value("")), - ), - ), - # If neither, use an empty string - default=Value(""), - output_field=CharField(), - ), - domain_info=ArrayAgg( - # an array of domains, with id and name, colon separated - Concat( - F("user__permissions__domain_id"), - Value(":"), - F("user__permissions__domain__name"), - # specify the output_field to ensure union has same column types - output_field=CharField(), - ), - distinct=True, - filter=Q(user__permissions__domain__isnull=False) # filter out null values - & Q( - user__permissions__domain__domain_info__portfolio=portfolio - ), # only include domains in portfolio - ), - source=Value("permission", output_field=CharField()), - ) - .values( - "id", - "first_name", - "last_name", - "email_display", - "last_active", - "roles", - "additional_permissions_display", - "member_display", - "domain_info", - "source", - ) - ) - return permissions - - def initial_invitations_search(self, portfolio): - """Perform initial invitations search and get related DomainInvitation data based on the email.""" - # Get DomainInvitation query for matching email and for the portfolio - domain_invitations = DomainInvitation.objects.filter( - email=OuterRef("email"), # Check if email matches the OuterRef("email") - domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio - ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) - # PortfolioInvitation query - invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) - invitations = invitations.annotate( - first_name=Value(None, output_field=CharField()), - last_name=Value(None, output_field=CharField()), - email_display=F("email"), - last_active=Value("Invited", output_field=TextField()), - additional_permissions_display=F("additional_permissions"), - member_display=F("email"), - # Use ArrayRemove to return an empty list when no domain invitations are found - domain_info=ArrayRemove( - ArrayAgg( - Subquery(domain_invitations.values("domain_info")), - distinct=True, - ) - ), - source=Value("invitation", output_field=CharField()), - ).values( + queryset = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio) + return queryset.values( + "id", + "first_name", + "last_name", + "email_display", + "last_active", + "roles", + "additional_permissions_display", + "member_display", + "domain_info", + "source", + ) + + def initial_invitations_search(self, portfolio): + """Perform initial invitations search and get related DomainInvitation data based on the email.""" + # Get DomainInvitation query for matching email and for the portfolio + queryset = PortfolioInvitationModelDict.get_annotated_queryset(portfolio) + return queryset.values( "id", "first_name", "last_name", @@ -153,7 +85,6 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): "domain_info", "source", ) - return invitations def apply_search_term(self, queryset, request): """Apply search term to the queryset.""" @@ -179,7 +110,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): queryset = queryset.order_by(sort_by) return queryset - def serialize_members(self, request, portfolio, item, user): + def serialize_members(self, portfolio, item, user): # Check if the user can edit other users user_can_edit_other_users = any( user.has_perm(perm) for perm in ["registrar.full_access_permission", "registrar.change_user"] @@ -213,9 +144,3 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): "svg_icon": ("visibility" if view_only else "settings"), } return member_json - - -# Custom Func to use array_remove to remove null values -class ArrayRemove(Func): - function = "array_remove" - template = "%(function)s(%(expressions)s, NULL)" From 48ae7f4c117af8491870e22e6a0cc46ce102055e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:31:59 -0700 Subject: [PATCH 028/148] Refactor part 2 --- .../templates/includes/members_table.html | 11 + src/registrar/utility/csv_export.py | 369 ++---------------- src/registrar/utility/model_dicts.py | 342 ++++++++++++++++ src/registrar/views/portfolio_members_json.py | 2 +- 4 files changed, 388 insertions(+), 336 deletions(-) create mode 100644 src/registrar/utility/model_dicts.py diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index 5e0dc6116..b1118abb4 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -36,6 +36,17 @@
+ {% comment %} {% if user_domain_count and user_domain_count > 0 %} {% endcomment %} + + {% comment %} {% endif %} {% endcomment %}
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index d39ab996c..4b01c7e45 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -20,11 +20,14 @@ from django.contrib.postgres.aggregates import StringAgg from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.models.utility.orm_helper import ArrayRemove +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.templatetags.custom_filters import get_region from registrar.utility.constants import BranchChoices from registrar.utility.enums import DefaultEmail from django.contrib.postgres.aggregates import ArrayAgg +from registrar.utility.model_dicts import BaseModelDict, PortfolioInvitationModelDict, UserPortfolioPermissionModelDict + logger = logging.getLogger(__name__) @@ -55,335 +58,6 @@ def format_end_date(end_date): return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date() -class BaseModelDict(ABC): - - @classmethod - @abstractmethod - def model(self): - """ - Property to specify the model that the export class will handle. - Must be implemented by subclasses. - """ - pass - - @classmethod - def get_sort_fields(cls): - """ - Returns the sort fields for the CSV export. Override in subclasses as needed. - """ - return [] - - @classmethod - def get_additional_args(cls): - """ - Returns additional keyword arguments as an empty dictionary. - Override in subclasses to provide specific arguments. - """ - return {} - - @classmethod - def get_select_related(cls): - """ - Get a list of tables to pass to select_related when building queryset. - """ - return [] - - @classmethod - def get_prefetch_related(cls): - """ - Get a list of tables to pass to prefetch_related when building queryset. - """ - return [] - - @classmethod - def get_exclusions(cls): - """ - Get a Q object of exclusion conditions to pass to .exclude() when building queryset. - """ - return Q() - - @classmethod - def get_filter_conditions(cls, **export_kwargs): - """ - Get a Q object of filter conditions to filter when building queryset. - """ - return Q() - - @classmethod - def get_annotated_fields(cls): - """ - Get a dict of computed fields. These are fields that do not exist on the model normally - and will be passed to .annotate() when building a queryset. - """ - return {} - - @classmethod - def get_annotations_for_sort(cls): - """ - Get a dict of annotations to make available for order_by clause. - """ - return {} - - @classmethod - def get_related_table_fields(cls): - """ - Get a list of fields from related tables. - """ - return [] - - @classmethod - def annotate_and_retrieve_fields( - cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs - ) -> QuerySet: - """ - Applies annotations to a queryset and retrieves specified fields, - including class-defined and annotation-defined. - - Parameters: - initial_queryset (QuerySet): Initial queryset. - annotated_fields (dict, optional): Fields to compute {field_name: expression}. - related_table_fields (list, optional): Extra fields to retrieve; defaults to annotation keys if None. - include_many_to_many (bool, optional): Determines if we should include many to many fields or not - **kwargs: Additional keyword arguments for specific parameters (e.g., public_contacts, domain_invitations, - user_domain_roles). - - Returns: - QuerySet: Contains dictionaries with the specified fields for each record. - """ - if related_table_fields is None: - related_table_fields = [] - - # We can infer that if we're passing in annotations, - # we want to grab the result of said annotation. - if annotated_fields: - related_table_fields.extend(annotated_fields.keys()) - - # Get prexisting fields on the model - model_fields = set() - for field in cls.model()._meta.get_fields(): - # Exclude many to many fields unless we specify - many_to_many = isinstance(field, ManyToManyField) and include_many_to_many - if many_to_many or not isinstance(field, ManyToManyField): - model_fields.add(field.name) - - queryset = initial_queryset.annotate(**annotated_fields).values(*model_fields, *related_table_fields) - - return cls.update_queryset(queryset, **kwargs) - - @classmethod - def get_annotated_queryset(cls, request=None): - sort_fields = cls.get_sort_fields() - kwargs = cls.get_additional_args() - select_related = cls.get_select_related() - prefetch_related = cls.get_prefetch_related() - exclusions = cls.get_exclusions() - annotations_for_sort = cls.get_annotations_for_sort() - filter_conditions = cls.get_filter_conditions(**kwargs) - annotated_fields = cls.get_annotated_fields() - related_table_fields = cls.get_related_table_fields() - - model_queryset = ( - cls.model() - .objects - .select_related(*select_related) - .prefetch_related(*prefetch_related) - .filter(filter_conditions) - .exclude(exclusions) - .annotate(**annotations_for_sort) - .order_by(*sort_fields) - .distinct() - ) - - return cls.annotate_and_retrieve_fields( - model_queryset, annotated_fields, related_table_fields, **kwargs - ) - - @classmethod - def update_queryset(cls, queryset, **kwargs): - """ - Returns an updated queryset. Override in subclass to update queryset. - """ - return queryset - - @classmethod - def get_models_dict(cls, request=None): - return convert_queryset_to_dict(cls.get_annotated_queryset(request), is_model=False) - - -class UserPortfolioPermissionModelDict(BaseModelDict): - - @classmethod - def model(cls): - # Return the model class that this export handles - return UserPortfolioPermission - - @classmethod - def get_select_related(cls): - """ - Get a list of tables to pass to select_related when building queryset. - """ - return ["user"] - - @classmethod - def get_filter_conditions(cls, portfolio): - """ - Get a Q object of filter conditions to filter when building queryset. - """ - if not portfolio: - # Return nothing - return Q(id__in=[]) - - # Get all members on this portfolio - return Q(portfolio=portfolio) - - @classmethod - def get_annotated_fields(cls, portfolio): - """ - Get a dict of computed fields. These are fields that do not exist on the model normally - and will be passed to .annotate() when building a queryset. - """ - if not portfolio: - # Return nothing - return {} - - return { - "first_name": F("user__first_name"), - "last_name": F("user__last_name"), - "email_display": F("user__email"), - "last_active": Coalesce( - Cast(F("user__last_login"), output_field=TextField()), - Value("Invalid date"), - output_field=TextField(), - ), - "additional_permissions_display": F("additional_permissions"), - "member_display": Case( - When( - Q(user__email__isnull=False) & ~Q(user__email=""), - then=F("user__email") - ), - When( - Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), - then=Concat( - Coalesce(F("user__first_name"), Value("")), - Value(" "), - Coalesce(F("user__last_name"), Value("")), - ), - ), - default=Value(""), - output_field=CharField(), - ), - "domain_info": ArrayAgg( - Concat( - F("user__permissions__domain_id"), - Value(":"), - F("user__permissions__domain__name"), - output_field=CharField(), - ), - distinct=True, - filter=Q(user__permissions__domain__isnull=False) - & Q(user__permissions__domain__domain_info__portfolio=portfolio), - ), - "source": Value("permission", output_field=CharField()), - } - - @classmethod - def get_annotated_queryset(cls, portfolio): - """Override of the base annotated queryset to pass in portfolio""" - model_queryset = ( - cls.model() - .objects - .select_related(*cls.get_select_related()) - .prefetch_related(*cls.get_prefetch_related()) - .filter(cls.get_filter_conditions(portfolio)) - .exclude(cls.get_exclusions()) - .annotate(**cls.get_annotations_for_sort()) - .order_by(*cls.get_sort_fields()) - .distinct() - ) - - annotated_fields = cls.get_annotated_fields(portfolio) - related_table_fields = cls.get_related_table_fields() - return cls.annotate_and_retrieve_fields( - model_queryset, annotated_fields, related_table_fields - ) - - -class PortfolioInvitationModelDict(BaseModelDict): - - @classmethod - def model(cls): - # Return the model class that this export handles - return PortfolioInvitation - - @classmethod - def get_filter_conditions(cls, portfolio): - """ - Get a Q object of filter conditions to filter when building queryset. - """ - if not portfolio: - # Return nothing - return Q(id__in=[]) - - # Get all members on this portfolio - return Q( - # Check if email matches the OuterRef("email") - email=OuterRef("email"), - # Check if the domain's portfolio matches the given portfolio) - domain__domain_info__portfolio=portfolio, - ) - - @classmethod - def get_annotated_fields(cls, portfolio): - """ - Get a dict of computed fields. These are fields that do not exist on the model normally - and will be passed to .annotate() when building a queryset. - """ - if not portfolio: - # Return nothing - return {} - - domain_invitations = DomainInvitation.objects.filter( - email=OuterRef("email"), # Check if email matches the OuterRef("email") - domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio - ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) - return { - "first_name": Value(None, output_field=CharField()), - "last_name": Value(None, output_field=CharField()), - "email_display": F("email"), - "last_active": Value("Invited", output_field=TextField()), - "additional_permissions_display": F("additional_permissions"), - "member_display": F("email"), - "domain_info": ArrayRemove( - ArrayAgg( - Subquery(domain_invitations.values("domain_info")), - distinct=True, - ) - ), - "source": Value("invitation", output_field=CharField()), - } - - @classmethod - def get_annotated_queryset(cls, portfolio): - """Override of the base annotated queryset to pass in portfolio""" - model_queryset = ( - cls.model() - .objects - .select_related(*cls.get_select_related()) - .prefetch_related(*cls.get_prefetch_related()) - .filter(cls.get_filter_conditions(portfolio)) - .exclude(cls.get_exclusions()) - .annotate(**cls.get_annotations_for_sort()) - .order_by(*cls.get_sort_fields()) - .distinct() - ) - - annotated_fields = cls.get_annotated_fields(portfolio) - related_table_fields = cls.get_related_table_fields() - return cls.annotate_and_retrieve_fields( - model_queryset, annotated_fields, related_table_fields - ) - - class BaseExport(BaseModelDict): """ A generic class for exporting data which returns a csv file for the given model. @@ -413,7 +87,7 @@ class BaseExport(BaseModelDict): """ writer = csv.writer(csv_file) columns = cls.get_columns() - models_dict = cls.get_models_dict() + models_dict = cls.get_models_dict(request=request) # Write to csv file before the write_csv cls.write_csv_before(writer, **export_kwargs) @@ -488,8 +162,18 @@ class MemberExport(BaseExport): """ Returns the columns for CSV export. Override in subclasses as needed. """ - return [] - + return [ + "Email", + "Organization admin", + "Invited by", + "Last active", + "Domain requests", + "Member management", + "Domain management", + "Number of domains", + "Domains", + ] + @classmethod @abstractmethod def parse_row(cls, columns, model): @@ -497,7 +181,22 @@ class MemberExport(BaseExport): Given a set of columns and a model dictionary, generate a new row from cleaned column data. Must be implemented by subclasses """ - pass + + is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (model.get("roles") or []) + FIELDS = { + "Email": model.get("email"), + "Organization admin": is_admin, + "Invited by": "TODO", + "Last active": "TODO", + "Domain requests": "TODO", + "Member management": "TODO", + "Domain management": "TODO", + "Number of domains": "TODO", + "Domains": "TODO", + } + + row = [FIELDS.get(column, "") for column in columns] + return row class DomainExport(BaseExport): """ @@ -757,7 +456,7 @@ class DomainDataType(DomainExport): """ Get a list of tables to pass to prefetch_related when building queryset. """ - return ["permissions"] + return ["domain__permissions"] @classmethod def get_annotated_fields(cls, delimiter=", "): @@ -797,7 +496,7 @@ class DomainDataTypeUser(DomainDataType): """ @classmethod - def get_filter_conditions(cls, request=None): + def get_filter_conditions(cls, request=None, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ diff --git a/src/registrar/utility/model_dicts.py b/src/registrar/utility/model_dicts.py new file mode 100644 index 000000000..d3f71aa1e --- /dev/null +++ b/src/registrar/utility/model_dicts.py @@ -0,0 +1,342 @@ +""" +TODO: explanation here +""" +from abc import ABC, abstractmethod +from registrar.models import ( + DomainInvitation, + PortfolioInvitation, +) +from django.db.models import Case, CharField, F, ManyToManyField, Q, QuerySet, Value, When, TextField, OuterRef, Subquery +from django.db.models.functions import Cast +from django.db.models.functions import Concat, Coalesce +from registrar.models.user_portfolio_permission import UserPortfolioPermission +from registrar.models.utility.generic_helper import convert_queryset_to_dict +from registrar.models.utility.orm_helper import ArrayRemove +from django.contrib.postgres.aggregates import ArrayAgg + + +class BaseModelDict(ABC): + + @classmethod + @abstractmethod + def model(self): + """ + Property to specify the model that the export class will handle. + Must be implemented by subclasses. + """ + pass + + @classmethod + def get_sort_fields(cls): + """ + Returns the sort fields for the CSV export. Override in subclasses as needed. + """ + return [] + + @classmethod + def get_additional_args(cls): + """ + Returns additional keyword arguments as an empty dictionary. + Override in subclasses to provide specific arguments. + """ + return {} + + @classmethod + def get_select_related(cls): + """ + Get a list of tables to pass to select_related when building queryset. + """ + return [] + + @classmethod + def get_prefetch_related(cls): + """ + Get a list of tables to pass to prefetch_related when building queryset. + """ + return [] + + @classmethod + def get_exclusions(cls): + """ + Get a Q object of exclusion conditions to pass to .exclude() when building queryset. + """ + return Q() + + @classmethod + def get_filter_conditions(cls, **export_kwargs): + """ + Get a Q object of filter conditions to filter when building queryset. + """ + return Q() + + @classmethod + def get_annotated_fields(cls): + """ + Get a dict of computed fields. These are fields that do not exist on the model normally + and will be passed to .annotate() when building a queryset. + """ + return {} + + @classmethod + def get_annotations_for_sort(cls): + """ + Get a dict of annotations to make available for order_by clause. + """ + return {} + + @classmethod + def get_related_table_fields(cls): + """ + Get a list of fields from related tables. + """ + return [] + + @classmethod + def annotate_and_retrieve_fields( + cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs + ) -> QuerySet: + """ + Applies annotations to a queryset and retrieves specified fields, + including class-defined and annotation-defined. + + Parameters: + initial_queryset (QuerySet): Initial queryset. + annotated_fields (dict, optional): Fields to compute {field_name: expression}. + related_table_fields (list, optional): Extra fields to retrieve; defaults to annotation keys if None. + include_many_to_many (bool, optional): Determines if we should include many to many fields or not + **kwargs: Additional keyword arguments for specific parameters (e.g., public_contacts, domain_invitations, + user_domain_roles). + + Returns: + QuerySet: Contains dictionaries with the specified fields for each record. + """ + if related_table_fields is None: + related_table_fields = [] + + # We can infer that if we're passing in annotations, + # we want to grab the result of said annotation. + if annotated_fields: + related_table_fields.extend(annotated_fields.keys()) + + # Get prexisting fields on the model + model_fields = set() + for field in cls.model()._meta.get_fields(): + # Exclude many to many fields unless we specify + many_to_many = isinstance(field, ManyToManyField) and include_many_to_many + if many_to_many or not isinstance(field, ManyToManyField): + model_fields.add(field.name) + + queryset = initial_queryset.annotate(**annotated_fields).values(*model_fields, *related_table_fields) + + return cls.update_queryset(queryset, **kwargs) + + @classmethod + def get_annotated_queryset(cls, **kwargs): + sort_fields = cls.get_sort_fields() + # Get additional args and merge with incoming kwargs + additional_args = cls.get_additional_args() + kwargs.update(additional_args) + select_related = cls.get_select_related() + prefetch_related = cls.get_prefetch_related() + exclusions = cls.get_exclusions() + annotations_for_sort = cls.get_annotations_for_sort() + filter_conditions = cls.get_filter_conditions(**kwargs) + annotated_fields = cls.get_annotated_fields() + related_table_fields = cls.get_related_table_fields() + + model_queryset = ( + cls.model() + .objects + .select_related(*select_related) + .prefetch_related(*prefetch_related) + .filter(filter_conditions) + .exclude(exclusions) + .annotate(**annotations_for_sort) + .order_by(*sort_fields) + .distinct() + ) + return cls.annotate_and_retrieve_fields( + model_queryset, annotated_fields, related_table_fields, **kwargs + ) + + @classmethod + def update_queryset(cls, queryset, **kwargs): + """ + Returns an updated queryset. Override in subclass to update queryset. + """ + return queryset + + @classmethod + def get_models_dict(cls, **kwargs): + request = kwargs.get("request") + print(f"get_models_dict => request is: {request}") + return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) + + +class UserPortfolioPermissionModelDict(BaseModelDict): + + @classmethod + def model(cls): + # Return the model class that this export handles + return UserPortfolioPermission + + @classmethod + def get_select_related(cls): + """ + Get a list of tables to pass to select_related when building queryset. + """ + return ["user"] + + @classmethod + def get_filter_conditions(cls, portfolio): + """ + Get a Q object of filter conditions to filter when building queryset. + """ + if not portfolio: + # Return nothing + return Q(id__in=[]) + + # Get all members on this portfolio + return Q(portfolio=portfolio) + + @classmethod + def get_annotated_fields(cls, portfolio): + """ + Get a dict of computed fields. These are fields that do not exist on the model normally + and will be passed to .annotate() when building a queryset. + """ + if not portfolio: + # Return nothing + return {} + + return { + "first_name": F("user__first_name"), + "last_name": F("user__last_name"), + "email_display": F("user__email"), + "last_active": Coalesce( + Cast(F("user__last_login"), output_field=TextField()), + Value("Invalid date"), + output_field=TextField(), + ), + "additional_permissions_display": F("additional_permissions"), + "member_display": Case( + When( + Q(user__email__isnull=False) & ~Q(user__email=""), + then=F("user__email") + ), + When( + Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), + then=Concat( + Coalesce(F("user__first_name"), Value("")), + Value(" "), + Coalesce(F("user__last_name"), Value("")), + ), + ), + default=Value(""), + output_field=CharField(), + ), + "domain_info": ArrayAgg( + Concat( + F("user__permissions__domain_id"), + Value(":"), + F("user__permissions__domain__name"), + output_field=CharField(), + ), + distinct=True, + filter=Q(user__permissions__domain__isnull=False) + & Q(user__permissions__domain__domain_info__portfolio=portfolio), + ), + "source": Value("permission", output_field=CharField()), + } + + @classmethod + def get_annotated_queryset(cls, portfolio): + """Override of the base annotated queryset to pass in portfolio""" + model_queryset = ( + cls.model() + .objects + .select_related(*cls.get_select_related()) + .prefetch_related(*cls.get_prefetch_related()) + .filter(cls.get_filter_conditions(portfolio)) + .exclude(cls.get_exclusions()) + .annotate(**cls.get_annotations_for_sort()) + .order_by(*cls.get_sort_fields()) + .distinct() + ) + + annotated_fields = cls.get_annotated_fields(portfolio) + related_table_fields = cls.get_related_table_fields() + return cls.annotate_and_retrieve_fields( + model_queryset, annotated_fields, related_table_fields + ) + + +class PortfolioInvitationModelDict(BaseModelDict): + + @classmethod + def model(cls): + # Return the model class that this export handles + return PortfolioInvitation + + @classmethod + def get_filter_conditions(cls, portfolio): + """ + Get a Q object of filter conditions to filter when building queryset. + """ + if not portfolio: + # Return nothing + return Q(id__in=[]) + + # Get all members on this portfolio + return Q(portfolio=portfolio) + + @classmethod + def get_annotated_fields(cls, portfolio): + """ + Get a dict of computed fields. These are fields that do not exist on the model normally + and will be passed to .annotate() when building a queryset. + """ + if not portfolio: + # Return nothing + return {} + + domain_invitations = DomainInvitation.objects.filter( + email=OuterRef("email"), # Check if email matches the OuterRef("email") + domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio + ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) + return { + "first_name": Value(None, output_field=CharField()), + "last_name": Value(None, output_field=CharField()), + "email_display": F("email"), + "last_active": Value("Invited", output_field=TextField()), + "additional_permissions_display": F("additional_permissions"), + "member_display": F("email"), + "domain_info": ArrayRemove( + ArrayAgg( + Subquery(domain_invitations.values("domain_info")), + distinct=True, + ) + ), + "source": Value("invitation", output_field=CharField()), + } + + @classmethod + def get_annotated_queryset(cls, portfolio): + """Override of the base annotated queryset to pass in portfolio""" + model_queryset = ( + cls.model() + .objects + .select_related(*cls.get_select_related()) + .prefetch_related(*cls.get_prefetch_related()) + .filter(cls.get_filter_conditions(portfolio)) + .exclude(cls.get_exclusions()) + .annotate(**cls.get_annotations_for_sort()) + .order_by(*cls.get_sort_fields()) + .distinct() + ) + + annotated_fields = cls.get_annotated_fields(portfolio) + related_table_fields = cls.get_related_table_fields() + return cls.annotate_and_retrieve_fields( + model_queryset, annotated_fields, related_table_fields + ) diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 408d37ff5..3a909810c 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -9,7 +9,7 @@ from django.views import View from registrar.models import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from registrar.utility.csv_export import UserPortfolioPermissionModelDict, PortfolioInvitationModelDict +from registrar.utility.model_dicts import PortfolioInvitationModelDict, UserPortfolioPermissionModelDict from registrar.views.utility.mixins import PortfolioMembersPermission From 1e43974a111beb419c595c2cd853d4b194f2f7fc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:11:38 -0700 Subject: [PATCH 029/148] view --- src/registrar/config/urls.py | 6 ++++++ src/registrar/templates/includes/members_table.html | 2 +- src/registrar/views/report_views.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index d289eaf90..e5606b986 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -21,6 +21,7 @@ from registrar.views.report_views import ( ExportDomainRequestDataFull, ExportDataTypeUser, ExportDataTypeRequests, + ExportMembersPortfolio, ) # --jsons @@ -216,6 +217,11 @@ urlpatterns = [ name="get-rejection-email-for-user-json", ), path("admin/", admin.site.urls), + path( + "reports/export_members_portfolio/", + ExportMembersPortfolio.as_view(), + name="export_members_portfolio", + ), path( "reports/export_data_type_user/", ExportDataTypeUser.as_view(), diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index b1118abb4..ae430501d 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -39,7 +39,7 @@ {% comment %} {% if user_domain_count and user_domain_count > 0 %} {% endcomment %}
- + Export as CSV diff --git a/src/registrar/views/report_views.py b/src/registrar/views/report_views.py index d9c4d192c..d30852540 100644 --- a/src/registrar/views/report_views.py +++ b/src/registrar/views/report_views.py @@ -169,6 +169,18 @@ class ExportDataTypeUser(View): return response +class ExportMembersPortfolio(View): + """Returns a a members report for a given portfolio""" + + def get(self, request, *args, **kwargs): + portfolio = request.session.get("portfolio") + # match the CSV example with all the fields + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = f'attachment; filename="members-for-{portfolio}.csv"' + csv_export.MemberExport.export_data_to_csv(response, request=request) + return response + + class ExportDataTypeRequests(View): """Returns a domain requests report for a given user on the request""" From 9ac8a3e8bcbc0b5955b08e0c7243e162f1ce7617 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:22:41 -0700 Subject: [PATCH 030/148] Add some data to report --- src/registrar/utility/csv_export.py | 44 ++++++++++++++++----- src/registrar/utility/model_dicts.py | 58 ++++++++++++++++++++-------- src/registrar/views/report_views.py | 10 +++-- 3 files changed, 83 insertions(+), 29 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 4b01c7e45..2006a0b8d 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -151,11 +151,23 @@ class MemberExport(BaseExport): if not portfolio: return {} - # Union the two querysets to combine UserPortfolioPermission + invites - permissions = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio) - invitations = PortfolioInvitationModelDict.get_annotated_queryset(portfolio) - objects = permissions.union(invitations) - return convert_queryset_to_dict(objects, is_model=False) + # Union the two querysets to combine UserPortfolioPermission + invites. + # Unions cannot have a col mismatch, so we must clamp what is returned here. + shared_columns = [ + "id", + "first_name", + "last_name", + "email_display", + "last_active", + "roles", + "additional_permissions_display", + "member_display", + "domain_info", + "source", + ] + permissions = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) + invitations = PortfolioInvitationModelDict.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) + return convert_queryset_to_dict(permissions.union(invitations), is_model=False) @classmethod def get_columns(cls): @@ -183,18 +195,32 @@ class MemberExport(BaseExport): """ is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (model.get("roles") or []) + domains = ",".join(model.get("domain_info")) if model.get("domain_info") else "" FIELDS = { - "Email": model.get("email"), + "Email": model.get("email_display"), "Organization admin": is_admin, - "Invited by": "TODO", - "Last active": "TODO", + "Invited by": model.get("source"), + "Last active": model.get("last_active"), "Domain requests": "TODO", "Member management": "TODO", "Domain management": "TODO", "Number of domains": "TODO", - "Domains": "TODO", + # Quote enclose the domain list + # Note: this will only enclose when more than two items exist + "Domains": domains, } + # "id", + # "first_name", + # "last_name", + # "email_display", + # "last_active", + # "roles", + # "additional_permissions_display", + # "member_display", + # "domain_info", + # "source", + row = [FIELDS.get(column, "") for column in columns] return row diff --git a/src/registrar/utility/model_dicts.py b/src/registrar/utility/model_dicts.py index d3f71aa1e..859e8d3c1 100644 --- a/src/registrar/utility/model_dicts.py +++ b/src/registrar/utility/model_dicts.py @@ -6,9 +6,8 @@ from registrar.models import ( DomainInvitation, PortfolioInvitation, ) -from django.db.models import Case, CharField, F, ManyToManyField, Q, QuerySet, Value, When, TextField, OuterRef, Subquery -from django.db.models.functions import Cast -from django.db.models.functions import Concat, Coalesce +from django.db.models import CharField, F, ManyToManyField, Q, QuerySet, Value, TextField, OuterRef, Subquery, Func, Case, When +from django.db.models.functions import Concat, Coalesce, Cast from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.models.utility.orm_helper import ArrayRemove @@ -200,7 +199,7 @@ class UserPortfolioPermissionModelDict(BaseModelDict): return Q(portfolio=portfolio) @classmethod - def get_annotated_fields(cls, portfolio): + def get_annotated_fields(cls, portfolio, csv_report=False): """ Get a dict of computed fields. These are fields that do not exist on the model normally and will be passed to .annotate() when building a queryset. @@ -209,12 +208,34 @@ class UserPortfolioPermissionModelDict(BaseModelDict): # Return nothing return {} + # Tweak the queries slightly to only return the data we need. + # When returning data for the csv report we: + # 1. Only return the domain name for 'domain_info' + # 2. Return a formatted date for 'last_active' + # These are just optimizations that are better done in SQL as opposed to python. + if csv_report: + domain_query = F("user__permissions__domain__name") + last_active_query = Func( + F("user__last_login"), + Value("FMMonth DD, YYYY"), + function="to_char", + output_field=TextField() + ) + else: + domain_query = Concat( + F("user__permissions__domain_id"), + Value(":"), + F("user__permissions__domain__name"), + output_field=CharField(), + ) + last_active_query = Cast(F("user__last_login"), output_field=TextField()) + return { "first_name": F("user__first_name"), "last_name": F("user__last_name"), "email_display": F("user__email"), "last_active": Coalesce( - Cast(F("user__last_login"), output_field=TextField()), + last_active_query, Value("Invalid date"), output_field=TextField(), ), @@ -236,12 +257,7 @@ class UserPortfolioPermissionModelDict(BaseModelDict): output_field=CharField(), ), "domain_info": ArrayAgg( - Concat( - F("user__permissions__domain_id"), - Value(":"), - F("user__permissions__domain__name"), - output_field=CharField(), - ), + domain_query, distinct=True, filter=Q(user__permissions__domain__isnull=False) & Q(user__permissions__domain__domain_info__portfolio=portfolio), @@ -250,7 +266,7 @@ class UserPortfolioPermissionModelDict(BaseModelDict): } @classmethod - def get_annotated_queryset(cls, portfolio): + def get_annotated_queryset(cls, portfolio, csv_report=False): """Override of the base annotated queryset to pass in portfolio""" model_queryset = ( cls.model() @@ -264,7 +280,7 @@ class UserPortfolioPermissionModelDict(BaseModelDict): .distinct() ) - annotated_fields = cls.get_annotated_fields(portfolio) + annotated_fields = cls.get_annotated_fields(portfolio, csv_report) related_table_fields = cls.get_related_table_fields() return cls.annotate_and_retrieve_fields( model_queryset, annotated_fields, related_table_fields @@ -291,7 +307,7 @@ class PortfolioInvitationModelDict(BaseModelDict): return Q(portfolio=portfolio) @classmethod - def get_annotated_fields(cls, portfolio): + def get_annotated_fields(cls, portfolio, csv_report=False): """ Get a dict of computed fields. These are fields that do not exist on the model normally and will be passed to .annotate() when building a queryset. @@ -300,10 +316,18 @@ class PortfolioInvitationModelDict(BaseModelDict): # Return nothing return {} + # Tweak the queries slightly to only return the data we need + if csv_report: + domain_query = F("domain__name") + else: + domain_query = Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField()) + + # Get all existing domain invitations and search on that domain_invitations = DomainInvitation.objects.filter( email=OuterRef("email"), # Check if email matches the OuterRef("email") domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio - ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) + ).annotate(domain_info=domain_query) + return { "first_name": Value(None, output_field=CharField()), "last_name": Value(None, output_field=CharField()), @@ -321,7 +345,7 @@ class PortfolioInvitationModelDict(BaseModelDict): } @classmethod - def get_annotated_queryset(cls, portfolio): + def get_annotated_queryset(cls, portfolio, csv_report=False): """Override of the base annotated queryset to pass in portfolio""" model_queryset = ( cls.model() @@ -335,7 +359,7 @@ class PortfolioInvitationModelDict(BaseModelDict): .distinct() ) - annotated_fields = cls.get_annotated_fields(portfolio) + annotated_fields = cls.get_annotated_fields(portfolio, csv_report) related_table_fields = cls.get_related_table_fields() return cls.annotate_and_retrieve_fields( model_queryset, annotated_fields, related_table_fields diff --git a/src/registrar/views/report_views.py b/src/registrar/views/report_views.py index d30852540..3b0f790d3 100644 --- a/src/registrar/views/report_views.py +++ b/src/registrar/views/report_views.py @@ -173,10 +173,14 @@ class ExportMembersPortfolio(View): """Returns a a members report for a given portfolio""" def get(self, request, *args, **kwargs): - portfolio = request.session.get("portfolio") - # match the CSV example with all the fields + """Returns the members report""" + + portfolio_display = "portfolio" + if request.session.get("portfolio"): + portfolio_display = str(request.session.get("portfolio")).replace(" ", "-") + response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = f'attachment; filename="members-for-{portfolio}.csv"' + response["Content-Disposition"] = f'attachment; filename="members-for-{portfolio_display}.csv"' csv_export.MemberExport.export_data_to_csv(response, request=request) return response From 10c01870d70b35332f055b2376728632100e9429 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:53:13 -0700 Subject: [PATCH 031/148] Add some additional data + migration --- src/registrar/admin.py | 2 + ...0137_userportfoliopermission_invitation.py | 25 ++++ src/registrar/models/portfolio_invitation.py | 18 ++- .../models/user_portfolio_permission.py | 10 ++ src/registrar/utility/csv_export.py | 39 +++--- .../{model_dicts.py => model_annotations.py} | 112 ++++++++++++++++-- src/registrar/views/portfolio_members_json.py | 6 +- 7 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 src/registrar/migrations/0137_userportfoliopermission_invitation.py rename src/registrar/utility/{model_dicts.py => model_annotations.py} (72%) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0cab01d31..2d05e760b 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1275,6 +1275,8 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin): "get_roles", ] + readonly_fields = ["invitation"] + autocomplete_fields = ["user", "portfolio"] search_fields = ["user__first_name", "user__last_name", "user__email", "portfolio__organization_name"] search_help_text = "Search by first name, last name, email, or portfolio." diff --git a/src/registrar/migrations/0137_userportfoliopermission_invitation.py b/src/registrar/migrations/0137_userportfoliopermission_invitation.py new file mode 100644 index 000000000..d0bc6dd63 --- /dev/null +++ b/src/registrar/migrations/0137_userportfoliopermission_invitation.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.10 on 2024-11-14 17:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0136_domainrequest_requested_suborganization_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="userportfoliopermission", + name="invitation", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="created_user_portfolio_permission", + to="registrar.portfolioinvitation", + ), + ), + ] diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 61a6b7397..544d7d145 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -9,6 +9,8 @@ from registrar.models.user_portfolio_permission import UserPortfolioPermission from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField +from django.contrib.admin.models import LogEntry, ADDITION +from django.contrib.contenttypes.models import ContentType logger = logging.getLogger(__name__) @@ -65,6 +67,20 @@ class PortfolioInvitation(TimeStampedModel): protected=True, # can't alter state except through transition methods! ) + # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. + @property + def creator(self): + """Get the user who created this invitation from the audit log""" + content_type = ContentType.objects.get_for_model(self) + log_entry = LogEntry.objects.filter( + content_type=content_type, + object_id=self.pk, + action_flag=ADDITION + ).order_by("action_time").first() + + return log_entry.user if log_entry else None + + def __str__(self): return f"Invitation for {self.email} on {self.portfolio} is {self.status}" @@ -101,7 +117,7 @@ class PortfolioInvitation(TimeStampedModel): # and create a role for that user on this portfolio user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( - portfolio=self.portfolio, user=user + portfolio=self.portfolio, user=user, invitation=self ) if self.roles and len(self.roles) > 0: user_portfolio_permission.roles = self.roles diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 8d09562c2..e54bb5af5 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -65,6 +65,16 @@ class UserPortfolioPermission(TimeStampedModel): help_text="Select one or more additional permissions.", ) + # TODO - this needs a small script to update existing values + invitation = models.ForeignKey( + "registrar.PortfolioInvitation", + null=True, + blank=True, + # We don't want to accidentally delete invitations + on_delete=models.PROTECT, + related_name="created_user_portfolio_permission", + ) + def __str__(self): readable_roles = [] if self.roles: diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 2006a0b8d..6abc54f5d 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -26,7 +26,7 @@ from registrar.utility.constants import BranchChoices from registrar.utility.enums import DefaultEmail from django.contrib.postgres.aggregates import ArrayAgg -from registrar.utility.model_dicts import BaseModelDict, PortfolioInvitationModelDict, UserPortfolioPermissionModelDict +from registrar.utility.model_annotations import BaseModelAnnotation, PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation logger = logging.getLogger(__name__) @@ -58,7 +58,7 @@ def format_end_date(end_date): return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date() -class BaseExport(BaseModelDict): +class BaseExport(BaseModelAnnotation): """ A generic class for exporting data which returns a csv file for the given model. Base class in an inheritance tree of 3. @@ -87,13 +87,13 @@ class BaseExport(BaseModelDict): """ writer = csv.writer(csv_file) columns = cls.get_columns() - models_dict = cls.get_models_dict(request=request) + model_dict = cls.get_model_dict(request=request) # Write to csv file before the write_csv cls.write_csv_before(writer, **export_kwargs) # Write the csv file - rows = cls.write_csv(writer, columns, models_dict) + rows = cls.write_csv(writer, columns, model_dict) # Return rows that for easier parsing and testing return rows @@ -146,7 +146,7 @@ class MemberExport(BaseExport): return None @classmethod - def get_models_dict(cls, request=None): + def get_model_dict(cls, request=None): portfolio = request.session.get("portfolio") if not portfolio: return {} @@ -164,10 +164,13 @@ class MemberExport(BaseExport): "member_display", "domain_info", "source", + "invitation_date", + "invited_by", ] - permissions = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) - invitations = PortfolioInvitationModelDict.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) - return convert_queryset_to_dict(permissions.union(invitations), is_model=False) + permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) + invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) + queryset_dict = convert_queryset_to_dict(permissions.union(invitations), is_model=False) + return queryset_dict @classmethod def get_columns(cls): @@ -178,6 +181,7 @@ class MemberExport(BaseExport): "Email", "Organization admin", "Invited by", + "Invitation date", "Last active", "Domain requests", "Member management", @@ -195,19 +199,26 @@ class MemberExport(BaseExport): """ is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (model.get("roles") or []) - domains = ",".join(model.get("domain_info")) if model.get("domain_info") else "" + # Tracks if they can view, create requests, or not do anything + x = model.get("roles") + print(f"what are the roles? {x}") + domain_request_user_permission = None + + user_managed_domains = model.get("domain_info", []) + managed_domains_as_csv = ",".join(user_managed_domains) + # Whether they can make domain requests. Tentatively, I think the options as we currently understand would be: None, Viewer, Viewer Requester FIELDS = { "Email": model.get("email_display"), "Organization admin": is_admin, - "Invited by": model.get("source"), + "Invited by": model.get("invited_by"), + "Invitation date": model.get("invitation_date"), "Last active": model.get("last_active"), "Domain requests": "TODO", "Member management": "TODO", "Domain management": "TODO", - "Number of domains": "TODO", - # Quote enclose the domain list - # Note: this will only enclose when more than two items exist - "Domains": domains, + "Number of domains": len(user_managed_domains), + # TODO - this doesn't quote enclose with one record + "Domains": managed_domains_as_csv, } # "id", diff --git a/src/registrar/utility/model_dicts.py b/src/registrar/utility/model_annotations.py similarity index 72% rename from src/registrar/utility/model_dicts.py rename to src/registrar/utility/model_annotations.py index 859e8d3c1..7b6b2bf54 100644 --- a/src/registrar/utility/model_dicts.py +++ b/src/registrar/utility/model_annotations.py @@ -1,5 +1,24 @@ """ -TODO: explanation here +Model annotation classes. Intended to return django querysets with computed fields for api endpoints and our csv reports. + +Created to manage the complexity of the MembersTable and Members CSV report, as they require complex but common annotations. + +These classes provide consistent, reusable query transformations that: +1. Add computed fields via annotations +2. Handle related model data +3. Format fields for display +4. Standardize field names across different contexts + +Used by both API endpoints (e.g. portfolio members JSON) and data exports (e.g. CSV reports). + +Example: + # For a JSON table endpoint + permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio) + # Returns queryset with standardized fields for member tables + + # For a CSV export + permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio, csv_report=True) + # Returns same fields but formatted for CSV export """ from abc import ABC, abstractmethod from registrar.models import ( @@ -12,9 +31,19 @@ from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.models.utility.orm_helper import ArrayRemove from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.admin.models import LogEntry, ADDITION +from django.contrib.contenttypes.models import ContentType -class BaseModelDict(ABC): +class BaseModelAnnotation(ABC): + """ + Abstract base class that standardizes how models are annotated for csv exports or complex annotation queries. + For example, the Members table / csv export. + + Subclasses define model-specific annotations, filters, and field formatting while inheriting + common queryset building logic. + Intended ensure consistent data presentation across both table UI components and CSV exports. + """ @classmethod @abstractmethod @@ -166,14 +195,16 @@ class BaseModelDict(ABC): return queryset @classmethod - def get_models_dict(cls, **kwargs): - request = kwargs.get("request") - print(f"get_models_dict => request is: {request}") + def get_model_dict(cls, **kwargs): return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) -class UserPortfolioPermissionModelDict(BaseModelDict): - +class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): + """ + Annotates UserPortfolioPermission querysets with computed fields for member tables. + Handles formatting of user details, permissions, and related domain information + for both UI display and CSV export. + """ @classmethod def model(cls): # Return the model class that this export handles @@ -217,7 +248,7 @@ class UserPortfolioPermissionModelDict(BaseModelDict): domain_query = F("user__permissions__domain__name") last_active_query = Func( F("user__last_login"), - Value("FMMonth DD, YYYY"), + Value("YYYY-MM-DD"), function="to_char", output_field=TextField() ) @@ -263,6 +294,30 @@ class UserPortfolioPermissionModelDict(BaseModelDict): & Q(user__permissions__domain__domain_info__portfolio=portfolio), ), "source": Value("permission", output_field=CharField()), + "invitation_date": Coalesce( + Func( + F("invitation__created_at"), + Value("YYYY-MM-DD"), + function="to_char", + output_field=TextField() + ), + Value("Invalid date"), + output_field=TextField(), + ), + # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. + # Grab the invitation creator from the audit log. This will need to be replaced with a creator field. + # When that happens, just replace this with F("invitation__creator") + "invited_by": Coalesce( + Subquery( + LogEntry.objects.filter( + content_type=ContentType.objects.get_for_model(PortfolioInvitation), + object_id=Cast(OuterRef("invitation__id"), output_field=TextField()), # Look up the invitation's ID + action_flag=ADDITION + ).order_by("action_time").values("user__email")[:1] + ), + Value("Unknown"), + output_field=CharField() + ), } @classmethod @@ -287,13 +342,25 @@ class UserPortfolioPermissionModelDict(BaseModelDict): ) -class PortfolioInvitationModelDict(BaseModelDict): +class PortfolioInvitationModelAnnotation(BaseModelAnnotation): + """ + Annotates PortfolioInvitation querysets with computed fields for the member table. + Handles formatting of user details, permissions, and related domain information + for both UI display and CSV export. + """ @classmethod def model(cls): # Return the model class that this export handles return PortfolioInvitation + @classmethod + def get_exclusions(cls): + """ + Get a Q object of exclusion conditions to pass to .exclude() when building queryset. + """ + return Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) + @classmethod def get_filter_conditions(cls, portfolio): """ @@ -322,7 +389,7 @@ class PortfolioInvitationModelDict(BaseModelDict): else: domain_query = Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField()) - # Get all existing domain invitations and search on that + # Get all existing domain invitations and search on that for domains the user exists on domain_invitations = DomainInvitation.objects.filter( email=OuterRef("email"), # Check if email matches the OuterRef("email") domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio @@ -342,6 +409,31 @@ class PortfolioInvitationModelDict(BaseModelDict): ) ), "source": Value("invitation", output_field=CharField()), + "invitation_date": Coalesce( + Func( + F("created_at"), + Value("YYYY-MM-DD"), + function="to_char", + output_field=TextField() + ), + Value("Invalid date"), + output_field=TextField(), + ), + # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. + # Grab the invitation creator from the audit log. This will need to be replaced with a creator field. + # When that happens, just replace this with F("invitation__creator") + "invited_by": Coalesce( + Subquery( + LogEntry.objects.filter( + content_type=ContentType.objects.get_for_model(PortfolioInvitation), + # Look up the invitation's ID. LogEntry expects a string as this it is stored as json. + object_id=Cast(OuterRef("id"), output_field=TextField()), + action_flag=ADDITION + ).order_by("action_time").values("user__email")[:1] + ), + Value("Unknown"), + output_field=CharField() + ), } @classmethod diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 3a909810c..2fe0492d6 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -9,7 +9,7 @@ from django.views import View from registrar.models import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from registrar.utility.model_dicts import PortfolioInvitationModelDict, UserPortfolioPermissionModelDict +from registrar.utility.model_annotations import PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation from registrar.views.utility.mixins import PortfolioMembersPermission @@ -55,7 +55,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): def initial_permissions_search(self, portfolio): """Perform initial search for permissions before applying any filters.""" - queryset = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio) + queryset = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio) return queryset.values( "id", "first_name", @@ -72,7 +72,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): def initial_invitations_search(self, portfolio): """Perform initial invitations search and get related DomainInvitation data based on the email.""" # Get DomainInvitation query for matching email and for the portfolio - queryset = PortfolioInvitationModelDict.get_annotated_queryset(portfolio) + queryset = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio) return queryset.values( "id", "first_name", From b7f3f083fcc14a03068ae2eab9cb46e4b4f437bb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:22:05 -0700 Subject: [PATCH 032/148] Cleanup --- src/registrar/utility/csv_export.py | 8 ++-- src/registrar/utility/model_annotations.py | 48 ++++------------------ 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 6abc54f5d..a9d5c9770 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -496,7 +496,7 @@ class DomainDataType(DomainExport): return ["domain__permissions"] @classmethod - def get_annotated_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ @@ -713,7 +713,7 @@ class DomainDataFull(DomainExport): ) @classmethod - def get_annotated_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ @@ -808,7 +808,7 @@ class DomainDataFederal(DomainExport): ) @classmethod - def get_annotated_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ @@ -1427,7 +1427,7 @@ class DomainRequestDataFull(DomainRequestExport): ] @classmethod - def get_annotated_fields(cls, delimiter=", "): + def get_annotated_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 7b6b2bf54..14056a531 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -91,14 +91,14 @@ class BaseModelAnnotation(ABC): return Q() @classmethod - def get_filter_conditions(cls, **export_kwargs): + def get_filter_conditions(cls, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ return Q() @classmethod - def get_annotated_fields(cls): + def get_annotated_fields(cls, **kwargs): """ Get a dict of computed fields. These are fields that do not exist on the model normally and will be passed to .annotate() when building a queryset. @@ -169,7 +169,7 @@ class BaseModelAnnotation(ABC): exclusions = cls.get_exclusions() annotations_for_sort = cls.get_annotations_for_sort() filter_conditions = cls.get_filter_conditions(**kwargs) - annotated_fields = cls.get_annotated_fields() + annotated_fields = cls.get_annotated_fields(**kwargs) related_table_fields = cls.get_related_table_fields() model_queryset = ( @@ -218,7 +218,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): return ["user"] @classmethod - def get_filter_conditions(cls, portfolio): + def get_filter_conditions(cls, portfolio, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -319,27 +319,11 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): output_field=CharField() ), } - + @classmethod def get_annotated_queryset(cls, portfolio, csv_report=False): """Override of the base annotated queryset to pass in portfolio""" - model_queryset = ( - cls.model() - .objects - .select_related(*cls.get_select_related()) - .prefetch_related(*cls.get_prefetch_related()) - .filter(cls.get_filter_conditions(portfolio)) - .exclude(cls.get_exclusions()) - .annotate(**cls.get_annotations_for_sort()) - .order_by(*cls.get_sort_fields()) - .distinct() - ) - - annotated_fields = cls.get_annotated_fields(portfolio, csv_report) - related_table_fields = cls.get_related_table_fields() - return cls.annotate_and_retrieve_fields( - model_queryset, annotated_fields, related_table_fields - ) + return super().get_annotated_queryset(portfolio=portfolio, csv_report=csv_report) class PortfolioInvitationModelAnnotation(BaseModelAnnotation): @@ -362,7 +346,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): return Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) @classmethod - def get_filter_conditions(cls, portfolio): + def get_filter_conditions(cls, portfolio, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -439,20 +423,4 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): @classmethod def get_annotated_queryset(cls, portfolio, csv_report=False): """Override of the base annotated queryset to pass in portfolio""" - model_queryset = ( - cls.model() - .objects - .select_related(*cls.get_select_related()) - .prefetch_related(*cls.get_prefetch_related()) - .filter(cls.get_filter_conditions(portfolio)) - .exclude(cls.get_exclusions()) - .annotate(**cls.get_annotations_for_sort()) - .order_by(*cls.get_sort_fields()) - .distinct() - ) - - annotated_fields = cls.get_annotated_fields(portfolio, csv_report) - related_table_fields = cls.get_related_table_fields() - return cls.annotate_and_retrieve_fields( - model_queryset, annotated_fields, related_table_fields - ) + return super().get_annotated_queryset(portfolio=portfolio, csv_report=csv_report) From 24126f99469423721ed2e530e2c4eb1bec0ec215 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 14 Nov 2024 15:32:42 -0500 Subject: [PATCH 033/148] breadcrumbs plus invited users on domain overview page --- src/registrar/templates/domain_add_user.html | 20 ++++++++++++++ src/registrar/templates/domain_detail.html | 23 +++++++++++++++- src/registrar/templates/domain_dns.html | 18 +++++++++++++ src/registrar/templates/domain_dnssec.html | 22 ++++++++++++++++ src/registrar/templates/domain_dsdata.html | 26 +++++++++++++++++++ .../templates/domain_nameservers.html | 22 ++++++++++++++++ .../templates/domain_security_email.html | 19 ++++++++++++++ .../templates/domain_suborganization.html | 21 +++++++++++++++ src/registrar/templates/domain_users.html | 19 ++++++++++++++ .../templates/includes/summary_item.html | 20 ++++++++++++++ 10 files changed, 209 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index fa3f8e821..1429127e6 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -5,6 +5,25 @@ {% block domain_content %} {% block breadcrumb %} + {% if portfolio %} + + + {% else %} {% url 'domain-users' pk=domain.id as url %} + {% endif %} {% endblock breadcrumb %}

Add a domain manager

{% if has_organization_feature_flag %} diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 0fb29d2eb..a245e5f25 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -3,7 +3,24 @@ {% load custom_filters %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {{ block.super }} +

{{ domain.name }}

{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html index 291319a59..9a2070c64 100644 --- a/src/registrar/templates/domain_dns.html +++ b/src/registrar/templates/domain_dns.html @@ -4,6 +4,24 @@ {% block title %}DNS | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %}

DNS

diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 7742a329b..cfec053c2 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -5,6 +5,28 @@ {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} +

DNSSEC

DNSSEC, or DNS Security Extensions, is an additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index ba742ab09..0f60235e1 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -4,6 +4,32 @@ {% block title %}DS data | {{ domain.name }} | {% endblock %} {% block domain_content %} + + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {% if domain.dnssecdata is None %}
diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index cc1fc0164..a5fd171a2 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -4,6 +4,28 @@ {% block title %}DNS name servers | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {# this is right after the messages block in the parent template #} {% for form in formset %} {% include "includes/form_errors.html" with form=form %} diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index e1755f85e..f5a58eb5d 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -4,6 +4,25 @@ {% block title %}Security email | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {% include "includes/form_errors.html" with form=form %}

Security email

diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html index 67726e9d5..648563d58 100644 --- a/src/registrar/templates/domain_suborganization.html +++ b/src/registrar/templates/domain_suborganization.html @@ -4,9 +4,30 @@ {% block title %}Suborganization{% if suborganization_name %} | suborganization_name{% endif %} | {% endblock %} {% block domain_content %} + + {% block breadcrumb %} + {% if portfolio %} + + + {% endif %} + {% endblock breadcrumb %} + {# this is right after the messages block in the parent template #} {% include "includes/form_errors.html" with form=form %} +

Suborganization

diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index a2eb3e604..6e0c0896c 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -4,6 +4,25 @@ {% block title %}Domain managers | {{ domain.name }} | {% endblock %} {% block domain_content %} + {% block breadcrumb %} + {% if portfolio %} + +

+ {% endif %} + {% endblock breadcrumb %} +

Domain managers

diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 0600d7ea7..c3b3fdd3f 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -106,6 +106,26 @@ {% endfor %} {% endif %} + {% elif domain_permissions %} + {% if value.permissions.all %} + {% if value.permissions|length == 1 %} +

{{ value.permissions.0.user.email }}

+ {% else %} +
    + {% for item in value.permissions.all %} +
  • {{ item.user.email }}
  • + {% endfor %} +
+ {% endif %} + {% endif %} + {% if value.invitations.all %} +

Invitations

+
    + {% for item in value.invitations.all %} +
  • {{ item.email }}
  • + {% endfor %} +
+ {% endif %} {% else %}

{% if value %} From 174d217315ddc52564f311e45ae24492cf16fcfa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:52:45 -0700 Subject: [PATCH 034/148] add info about roles / perms --- .../models/user_portfolio_permission.py | 27 +++++++++++++++++++ src/registrar/utility/csv_export.py | 20 ++++++-------- src/registrar/utility/model_annotations.py | 2 +- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index e54bb5af5..5a26f350e 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -115,6 +115,33 @@ class UserPortfolioPermission(TimeStampedModel): if additional_permissions: portfolio_permissions.update(additional_permissions) return list(portfolio_permissions) + + @classmethod + def get_domain_request_permission_display(cls, roles, additional_permissions): + """Class method to return a readable string for domain request permissions""" + # Tracks if they can view, create requests, or not do anything + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions) + all_domain_perms = [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS] + if (all(perm in all_permissions for perm in all_domain_perms)): + return "Viewer Requester" + elif (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions): + return "Viewer" + else: + return "None" + + @classmethod + def get_member_permission_display(cls, roles, additional_permissions): + """Class method to return a readable string for member permissions""" + # Tracks if they can view, create requests, or not do anything + all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions) + # Note for reviewers: the reason why this isn't checking on "all" is because + # the way perms work for members is different than requests. We need to consolidate this. + if (UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions): + return "Manager" + elif (UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions): + return "Viewer" + else: + return "None" def clean(self): """Extends clean method to perform additional validation, which can raise errors in django admin.""" diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index a9d5c9770..401cf37b5 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -10,7 +10,6 @@ from registrar.models import ( DomainInformation, PublicContact, UserDomainRole, - PortfolioInvitation, ) from django.db.models import Case, CharField, Count, DateField, F, ManyToManyField, Q, QuerySet, Value, When, TextField, OuterRef, Subquery from django.db.models.functions import Cast @@ -20,7 +19,7 @@ from django.contrib.postgres.aggregates import StringAgg from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.models.utility.orm_helper import ArrayRemove -from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices, UserPortfolioPermissionChoices from registrar.templatetags.custom_filters import get_region from registrar.utility.constants import BranchChoices from registrar.utility.enums import DefaultEmail @@ -197,24 +196,21 @@ class MemberExport(BaseExport): Given a set of columns and a model dictionary, generate a new row from cleaned column data. Must be implemented by subclasses """ - - is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (model.get("roles") or []) - # Tracks if they can view, create requests, or not do anything - x = model.get("roles") - print(f"what are the roles? {x}") - domain_request_user_permission = None - + roles = model.get("roles") + additional_permissions = model.get("additional_permissions_display") + is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (roles or []) + domain_request_display = UserPortfolioPermission.get_domain_request_permission_display(roles, additional_permissions) + member_perm_display = UserPortfolioPermission.get_member_permission_display(roles, additional_permissions) user_managed_domains = model.get("domain_info", []) managed_domains_as_csv = ",".join(user_managed_domains) - # Whether they can make domain requests. Tentatively, I think the options as we currently understand would be: None, Viewer, Viewer Requester FIELDS = { "Email": model.get("email_display"), "Organization admin": is_admin, "Invited by": model.get("invited_by"), "Invitation date": model.get("invitation_date"), "Last active": model.get("last_active"), - "Domain requests": "TODO", - "Member management": "TODO", + "Domain requests": domain_request_display, + "Member management": member_perm_display, "Domain management": "TODO", "Number of domains": len(user_managed_domains), # TODO - this doesn't quote enclose with one record diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 14056a531..5fc868462 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -241,7 +241,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): # Tweak the queries slightly to only return the data we need. # When returning data for the csv report we: - # 1. Only return the domain name for 'domain_info' + # 1. Only return the domain name for 'domain_info' rather than also add ':' seperated id # 2. Return a formatted date for 'last_active' # These are just optimizations that are better done in SQL as opposed to python. if csv_report: From bf585bec2afb794d036ec1118a23f5376e7082c4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:57:28 -0700 Subject: [PATCH 035/148] add domain management portion --- src/registrar/utility/csv_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 401cf37b5..c8dde131e 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -211,7 +211,7 @@ class MemberExport(BaseExport): "Last active": model.get("last_active"), "Domain requests": domain_request_display, "Member management": member_perm_display, - "Domain management": "TODO", + "Domain management": len(user_managed_domains) > 0, "Number of domains": len(user_managed_domains), # TODO - this doesn't quote enclose with one record "Domains": managed_domains_as_csv, From 0ca0602b0735b8b2c9695626655a953d7fc85ebe Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:01:15 -0700 Subject: [PATCH 036/148] Update portfolio_invitation.py --- src/registrar/models/portfolio_invitation.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 544d7d145..0ea410211 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -67,20 +67,6 @@ class PortfolioInvitation(TimeStampedModel): protected=True, # can't alter state except through transition methods! ) - # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. - @property - def creator(self): - """Get the user who created this invitation from the audit log""" - content_type = ContentType.objects.get_for_model(self) - log_entry = LogEntry.objects.filter( - content_type=content_type, - object_id=self.pk, - action_flag=ADDITION - ).order_by("action_time").first() - - return log_entry.user if log_entry else None - - def __str__(self): return f"Invitation for {self.email} on {self.portfolio} is {self.status}" From 78e03160aa485e60f0949ebc685f088b96d5a452 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:04:51 -0700 Subject: [PATCH 037/148] Delete 0137_userportfoliopermission_invitation.py --- ...0137_userportfoliopermission_invitation.py | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/registrar/migrations/0137_userportfoliopermission_invitation.py diff --git a/src/registrar/migrations/0137_userportfoliopermission_invitation.py b/src/registrar/migrations/0137_userportfoliopermission_invitation.py deleted file mode 100644 index d0bc6dd63..000000000 --- a/src/registrar/migrations/0137_userportfoliopermission_invitation.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.10 on 2024-11-14 17:54 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0136_domainrequest_requested_suborganization_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="userportfoliopermission", - name="invitation", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="created_user_portfolio_permission", - to="registrar.portfolioinvitation", - ), - ), - ] From 17acb664a510391a219987f32b95f80619850b6b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:05:32 -0700 Subject: [PATCH 038/148] readd migration --- ...0138_userportfoliopermission_invitation.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/registrar/migrations/0138_userportfoliopermission_invitation.py diff --git a/src/registrar/migrations/0138_userportfoliopermission_invitation.py b/src/registrar/migrations/0138_userportfoliopermission_invitation.py new file mode 100644 index 000000000..abac42ae2 --- /dev/null +++ b/src/registrar/migrations/0138_userportfoliopermission_invitation.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.10 on 2024-11-14 21:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0137_suborganization_city_suborganization_state_territory"), + ] + + operations = [ + migrations.AddField( + model_name="userportfoliopermission", + name="invitation", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="created_user_portfolio_permission", + to="registrar.portfolioinvitation", + ), + ), + ] From ae6ecabfc020c3dc7a4927e2219800f87ddefb62 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:19:33 -0700 Subject: [PATCH 039/148] Update csv_export.py --- src/registrar/utility/csv_export.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index c8dde131e..a6cf53c0f 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -547,7 +547,7 @@ class DomainRequestsDataType: """ @classmethod - def get_filter_conditions(cls, request=None): + def get_filter_conditions(cls, request=None, **kwargs): if request is None or not hasattr(request, "user") or not request.user.is_authenticated: return Q(id__in=[]) @@ -697,7 +697,7 @@ class DomainDataFull(DomainExport): return ["domain"] @classmethod - def get_filter_conditions(cls): + def get_filter_conditions(cls, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -791,7 +791,7 @@ class DomainDataFederal(DomainExport): return ["domain"] @classmethod - def get_filter_conditions(cls): + def get_filter_conditions(cls, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -888,7 +888,7 @@ class DomainGrowth(DomainExport): return ["domain"] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None): + def get_filter_conditions(cls, start_date=None, end_date=None, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -960,7 +960,7 @@ class DomainManaged(DomainExport): return ["permissions"] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None): + def get_filter_conditions(cls, start_date=None, end_date=None, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -1095,7 +1095,7 @@ class DomainUnmanaged(DomainExport): return ["permissions"] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None): + def get_filter_conditions(cls, start_date=None, end_date=None, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -1327,7 +1327,7 @@ class DomainRequestGrowth(DomainRequestExport): ] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None): + def get_filter_conditions(cls, start_date=None, end_date=None, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ From b57ab6eea1a21c396ae589b93d2953c9c07266d0 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 14 Nov 2024 17:35:40 -0500 Subject: [PATCH 040/148] formatting of domain overview, invited domain mgr label --- src/registrar/assets/sass/_theme/_forms.scss | 5 +++-- src/registrar/assets/sass/_theme/_typography.scss | 7 +++++++ src/registrar/templates/includes/summary_item.html | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 08e35b19f..9158de174 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -1,5 +1,6 @@ @use "uswds-core" as *; @use "cisa_colors" as *; +@use "typography" as *; .usa-form .usa-button { margin-top: units(3); @@ -69,9 +70,9 @@ legend.float-left-tablet + button.float-right-tablet { } .read-only-label { - font-size: size('body', 'sm'); + @extend .h4--sm-05; + font-weight: bold; color: color('primary-dark'); - margin-bottom: units(0.5); } .read-only-value { diff --git a/src/registrar/assets/sass/_theme/_typography.scss b/src/registrar/assets/sass/_theme/_typography.scss index d815ef6dd..466b6f975 100644 --- a/src/registrar/assets/sass/_theme/_typography.scss +++ b/src/registrar/assets/sass/_theme/_typography.scss @@ -23,6 +23,13 @@ h2 { color: color('primary-darker'); } +.h4--sm-05 { + font-size: size('body', 'sm'); + font-weight: normal; + color: color('primary'); + margin-bottom: units(0.5); +} + // Normalize typography in forms .usa-form, .usa-form fieldset { diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index c3b3fdd3f..15cc0f67f 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -119,7 +119,7 @@ {% endif %} {% endif %} {% if value.invitations.all %} -

Invitations

+

Invited domain managers

    {% for item in value.invitations.all %}
  • {{ item.email }}
  • From 450196e7e2c54bd58119a2de4ae33cb614734688 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 14 Nov 2024 18:17:07 -0500 Subject: [PATCH 041/148] added is_portfolio_admin and custom message in domain overview for admin who is not a manager --- src/registrar/context_processors.py | 2 ++ src/registrar/models/user.py | 3 +++ src/registrar/templates/domain_detail.html | 10 +++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 53f6e8ae7..c2f3e6f79 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -68,6 +68,7 @@ def portfolio_permissions(request): "has_organization_feature_flag": False, "has_organization_requests_flag": False, "has_organization_members_flag": False, + "is_portfolio_admin": False, } try: portfolio = request.session.get("portfolio") @@ -88,6 +89,7 @@ def portfolio_permissions(request): "has_organization_feature_flag": True, "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), } return portfolio_context diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 80c972d38..6d4453e88 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -257,6 +257,9 @@ class User(AbstractUser): def has_edit_suborganization_portfolio_permission(self, portfolio): return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION) + def is_portfolio_admin(self, portfolio): + return self.has_edit_suborganization_portfolio_permission(portfolio) + 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 a245e5f25..c13402066 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -58,7 +58,15 @@ {% include "includes/domain_dates.html" %} - {% if is_portfolio_user and not is_domain_manager %} + {% if is_portfolio_admin and not is_domain_manager %} +
    +
    +

    + To manage information for this domain, you must add yourself as a domain manager. +

    +
    +
    + {% elif is_portfolio_user and not is_domain_manager %}

    From 6f0bc4ce6c63eaa24f198b02af87492594679b76 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 15 Nov 2024 07:21:20 -0500 Subject: [PATCH 042/148] unit test --- src/registrar/tests/test_models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 53206359b..90d74c158 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -824,6 +824,15 @@ class TestUser(TestCase): cm.exception.message, "When portfolio roles or additional permissions are assigned, portfolio is required." ) + @less_console_noise_decorator + def test_user_with_admin_portfolio_role(self): + portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") + self.assertFalse(self.user.is_portfolio_admin(portfolio)) + UserPortfolioPermission.objects.get_or_create( + portfolio=portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + self.assertTrue(self.user.is_portfolio_admin(portfolio)) + class TestContact(TestCase): @less_console_noise_decorator From 17fa1ae00856c0302ca8da9b04b18ffcb4e9007a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 15 Nov 2024 08:20:17 -0500 Subject: [PATCH 043/148] view / manage button on suborganization section --- src/registrar/templates/domain_detail.html | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index c13402066..e2c2d0129 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -97,9 +97,16 @@ {% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %} {% endif %} - {% if portfolio and has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %} - {% url 'domain-suborganization' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %} + {% if portfolio %} + {% if has_any_domains_portfolio_permission and has_edit_suborganization_portfolio_permission %} + {% url 'domain-suborganization' pk=domain.id as url %} + {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %} + {% elif has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %} + {% url 'domain-suborganization' pk=domain.id as url %} + {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_view_suborganization_portfolio_permission view_button=True %} + + + {% endif %} {% else %} {% url 'domain-org-name-address' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %} From a09a53e6002c32dc808fd36c4c6f330de9e8d1ca Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 15 Nov 2024 08:45:54 -0500 Subject: [PATCH 044/148] updated view tests, and linted --- src/registrar/models/user.py | 2 +- src/registrar/tests/test_views_domain.py | 39 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 6d4453e88..a51c0f8c6 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -259,7 +259,7 @@ class User(AbstractUser): def is_portfolio_admin(self, portfolio): return self.has_edit_suborganization_portfolio_permission(portfolio) - + def get_first_portfolio(self): permission = self.portfolio_permissions.first() if permission: diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index a375493be..453f79481 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -6,7 +6,7 @@ from django.urls import reverse from django.contrib.auth import get_user_model from waffle.testutils import override_flag from api.tests.common import less_console_noise_decorator -from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from .common import MockEppLib, MockSESClient, create_user # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -342,7 +342,10 @@ class TestDomainDetail(TestDomainOverview): DomainInformation.objects.get_or_create(creator=user, domain=domain, portfolio=portfolio) UserPortfolioPermission.objects.get_or_create( - user=user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + user=user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, + ], ) user.refresh_from_db() self.client.force_login(user) @@ -357,6 +360,38 @@ class TestDomainDetail(TestDomainOverview): # Check that user does not have option to Edit domain self.assertNotContains(detail_page, "Edit") + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + def test_domain_readonly_on_detail_page_for_org_admin_not_manager(self): + """Test that a domain, which is part of a portfolio, but for which the user is not a domain manager, + properly displays read only""" + + portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user) + # need to create a different user than self.user because the user needs permission assignments + user = get_user_model().objects.create( + first_name="Test", + last_name="User", + email="bogus@example.gov", + phone="8003111234", + title="test title", + ) + domain, _ = Domain.objects.get_or_create(name="bogusdomain.gov") + DomainInformation.objects.get_or_create(creator=user, domain=domain, portfolio=portfolio) + + UserPortfolioPermission.objects.get_or_create( + user=user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + user.refresh_from_db() + self.client.force_login(user) + detail_page = self.client.get(f"/domain/{domain.id}") + # Check that alert message displays properly + self.assertContains( + detail_page, + "To manage information for this domain, you must add yourself as a domain manager.", + ) + # Check that user does not have option to Edit domain + self.assertNotContains(detail_page, "Edit") + class TestDomainManagers(TestDomainOverview): @classmethod From 837d618015b662c4850affeb44d2c7ba542e369f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 15 Nov 2024 08:52:50 -0500 Subject: [PATCH 045/148] linted some more --- src/registrar/tests/test_views_domain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 453f79481..98d0df038 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -342,7 +342,9 @@ class TestDomainDetail(TestDomainOverview): DomainInformation.objects.get_or_create(creator=user, domain=domain, portfolio=portfolio) UserPortfolioPermission.objects.get_or_create( - user=user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + user=user, + portfolio=portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], additional_permissions=[ UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, ], From e915937510bb5d3242f8bbc030eccafa31ef546f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:39:44 -0700 Subject: [PATCH 046/148] Update csv_export.py --- src/registrar/utility/csv_export.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index a6cf53c0f..a85732a3e 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -892,6 +892,10 @@ class DomainGrowth(DomainExport): """ Get a Q object of filter conditions to filter when building queryset. """ + if not start_date or not end_date: + # Return nothing + return Q(id__in=[]) + filter_ready = Q( domain__state__in=[Domain.State.READY], domain__first_ready__gte=start_date, @@ -960,10 +964,14 @@ class DomainManaged(DomainExport): return ["permissions"] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None, **kwargs): + def get_filter_conditions(cls, end_date=None, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ + if not end_date: + # Return nothing + return Q(id__in=[]) + end_date_formatted = format_end_date(end_date) return Q( domain__permissions__isnull=False, @@ -1095,10 +1103,14 @@ class DomainUnmanaged(DomainExport): return ["permissions"] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None, **kwargs): + def get_filter_conditions(cls, end_date=None, **kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ + if not end_date: + # Return nothing + return Q(id__in=[]) + end_date_formatted = format_end_date(end_date) return Q( domain__permissions__isnull=True, @@ -1331,6 +1343,9 @@ class DomainRequestGrowth(DomainRequestExport): """ Get a Q object of filter conditions to filter when building queryset. """ + if not start_date or not end_date: + # Return nothing + return Q(id__in=[]) start_date_formatted = format_start_date(start_date) end_date_formatted = format_end_date(end_date) From ea0f06b5eb6a132644a8a769a03c97b6a6d74487 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Fri, 15 Nov 2024 13:45:43 -0500 Subject: [PATCH 047/148] fixes --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/assets/sass/_theme/_base.scss | 2 +- src/registrar/tests/test_views_request.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index f6b087743..f204092be 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1814,7 +1814,7 @@ class DomainRequestsTable extends LoadTableBase { ${submissionDate} ${markupCreatorRow} - + ${request.status} diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 891f950e6..7d5a72a82 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -256,4 +256,4 @@ abbr[title] { .maxwidth-386{ max-width: 386px !important; -} \ No newline at end of file +} diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 41b80e12b..a73fac5a8 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -3214,7 +3214,6 @@ class TestDomainRequestWizard(TestWithUser, WebTest): self.fail(f"Expected a redirect, but got a different response: {response}") # Data cleanup - # test deployment user_portfolio_permission.delete() portfolio.delete() federal_agency.delete() From 9374d91c4b05370527b325fe2ded4e4b3dfc9284 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:41:29 -0700 Subject: [PATCH 048/148] Fix failing tests (hopefully) --- src/registrar/utility/csv_export.py | 12 ++++++------ src/registrar/utility/model_annotations.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index a85732a3e..cf9cd5a2b 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -71,7 +71,7 @@ class BaseExport(BaseModelAnnotation): return [] @classmethod - def write_csv_before(cls, csv_writer, **export_kwargs): + def write_csv_before(cls, csv_writer, **kwargs): """ Write to csv file before the write_csv method. Override in subclasses where needed. @@ -79,20 +79,20 @@ class BaseExport(BaseModelAnnotation): pass @classmethod - def export_data_to_csv(cls, csv_file, request=None, **export_kwargs): + def export_data_to_csv(cls, csv_file, **kwargs): """ All domain metadata: Exports domains of all statuses plus domain managers. """ writer = csv.writer(csv_file) columns = cls.get_columns() - model_dict = cls.get_model_dict(request=request) + models_dict = cls.get_model_annotation_dict(**kwargs) # Write to csv file before the write_csv - cls.write_csv_before(writer, **export_kwargs) + cls.write_csv_before(writer, **kwargs) # Write the csv file - rows = cls.write_csv(writer, columns, model_dict) + rows = cls.write_csv(writer, columns, models_dict) # Return rows that for easier parsing and testing return rows @@ -145,7 +145,7 @@ class MemberExport(BaseExport): return None @classmethod - def get_model_dict(cls, request=None): + def get_model_annotation_dict(cls, request=None, **kwargs): portfolio = request.session.get("portfolio") if not portfolio: return {} diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 5fc868462..38f00b072 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -195,7 +195,7 @@ class BaseModelAnnotation(ABC): return queryset @classmethod - def get_model_dict(cls, **kwargs): + def get_model_annotation_dict(cls, **kwargs): return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) From 7075b72533a464b1b34682fc5598c5fbcea97b22 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:30:36 -0700 Subject: [PATCH 049/148] lint + add invitation script --- ...te_user_portfolio_permission_invitation.py | 27 +++++++ .../models/user_portfolio_permission.py | 17 ++-- src/registrar/models/utility/orm_helper.py | 4 +- .../templates/includes/members_table.html | 4 +- src/registrar/utility/csv_export.py | 39 ++++++++-- src/registrar/utility/model_annotations.py | 78 ++++++++++--------- src/registrar/views/portfolio_members_json.py | 5 +- src/registrar/views/portfolios.py | 15 ++++ 8 files changed, 133 insertions(+), 56 deletions(-) create mode 100644 src/registrar/management/commands/populate_user_portfolio_permission_invitation.py diff --git a/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py b/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py new file mode 100644 index 000000000..6e72792ac --- /dev/null +++ b/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py @@ -0,0 +1,27 @@ +import logging +from django.core.management import BaseCommand +from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors, TerminalHelper +from registrar.models import UserPortfolioPermission, PortfolioInvitation +from auditlog.models import LogEntry + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand, PopulateScriptTemplate): + help = "Loops through each UserPortfolioPermission object and populates the invitation field" + + def handle(self, **kwargs): + """Loops through each DomainRequest object and populates + its last_status_update and first_submitted_date values""" + self.existing_invitations = PortfolioInvitation.objects.filter(portfolio__isnull=False, email__isnull=False).select_related('portfolio') + filter_condition = {"invitation__isnull": True, "portfolio__isnull": False, "user__email__isnull": False} + self.mass_update_records(UserPortfolioPermission, filter_condition, fields_to_update=["invitation"]) + + def update_record(self, record: UserPortfolioPermission): + """Associate the invitation to the right object""" + record.invitation = self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).first() + TerminalHelper.colorful_logger("INFO", "OKCYAN", f"{TerminalColors.OKCYAN}Adding invitation to {record}") + + def should_skip_record(self, record) -> bool: + """There is nothing to add if no invitation exists""" + return not record or not self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).exists() diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 5a26f350e..424f09a17 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -115,16 +115,19 @@ class UserPortfolioPermission(TimeStampedModel): if additional_permissions: portfolio_permissions.update(additional_permissions) return list(portfolio_permissions) - + @classmethod def get_domain_request_permission_display(cls, roles, additional_permissions): """Class method to return a readable string for domain request permissions""" # Tracks if they can view, create requests, or not do anything all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions) - all_domain_perms = [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS] - if (all(perm in all_permissions for perm in all_domain_perms)): + all_domain_perms = [ + UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, + UserPortfolioPermissionChoices.EDIT_REQUESTS, + ] + if all(perm in all_permissions for perm in all_domain_perms): return "Viewer Requester" - elif (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions): + elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions: return "Viewer" else: return "None" @@ -134,11 +137,11 @@ class UserPortfolioPermission(TimeStampedModel): """Class method to return a readable string for member permissions""" # Tracks if they can view, create requests, or not do anything all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions) - # Note for reviewers: the reason why this isn't checking on "all" is because + # Note for reviewers: the reason why this isn't checking on "all" is because # the way perms work for members is different than requests. We need to consolidate this. - if (UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions): + if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions: return "Manager" - elif (UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions): + elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions: return "Viewer" else: return "None" diff --git a/src/registrar/models/utility/orm_helper.py b/src/registrar/models/utility/orm_helper.py index 24f7982e7..4f4665216 100644 --- a/src/registrar/models/utility/orm_helper.py +++ b/src/registrar/models/utility/orm_helper.py @@ -1,6 +1,8 @@ from django.db.models.expressions import Func + class ArrayRemove(Func): """Custom Func to use array_remove to remove null values""" + function = "array_remove" - template = "%(function)s(%(expressions)s, NULL)" \ No newline at end of file + template = "%(function)s(%(expressions)s, NULL)" diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index ae430501d..09cebda2e 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -36,7 +36,7 @@

- {% comment %} {% if user_domain_count and user_domain_count > 0 %} {% endcomment %} + {% if member_count and member_count > 0 %} - {% comment %} {% endif %} {% endcomment %} + {% endif %}
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index cf9cd5a2b..76a84094c 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -11,7 +11,21 @@ from registrar.models import ( PublicContact, UserDomainRole, ) -from django.db.models import Case, CharField, Count, DateField, F, ManyToManyField, Q, QuerySet, Value, When, TextField, OuterRef, Subquery +from django.db.models import ( + Case, + CharField, + Count, + DateField, + F, + ManyToManyField, + Q, + QuerySet, + Value, + When, + TextField, + OuterRef, + Subquery, +) from django.db.models.functions import Cast from django.utils import timezone from django.db.models.functions import Concat, Coalesce @@ -25,7 +39,11 @@ from registrar.utility.constants import BranchChoices from registrar.utility.enums import DefaultEmail from django.contrib.postgres.aggregates import ArrayAgg -from registrar.utility.model_annotations import BaseModelAnnotation, PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation +from registrar.utility.model_annotations import ( + BaseModelAnnotation, + PortfolioInvitationModelAnnotation, + UserPortfolioPermissionModelAnnotation, +) logger = logging.getLogger(__name__) @@ -134,6 +152,7 @@ class BaseExport(BaseModelAnnotation): """ pass + class MemberExport(BaseExport): @classmethod @@ -143,7 +162,7 @@ class MemberExport(BaseExport): This is a special edge case, but the base report requires this to be defined. """ return None - + @classmethod def get_model_annotation_dict(cls, request=None, **kwargs): portfolio = request.session.get("portfolio") @@ -166,8 +185,12 @@ class MemberExport(BaseExport): "invitation_date", "invited_by", ] - permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) - invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) + permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values( + *shared_columns + ) + invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values( + *shared_columns + ) queryset_dict = convert_queryset_to_dict(permissions.union(invitations), is_model=False) return queryset_dict @@ -199,7 +222,9 @@ class MemberExport(BaseExport): roles = model.get("roles") additional_permissions = model.get("additional_permissions_display") is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (roles or []) - domain_request_display = UserPortfolioPermission.get_domain_request_permission_display(roles, additional_permissions) + domain_request_display = UserPortfolioPermission.get_domain_request_permission_display( + roles, additional_permissions + ) member_perm_display = UserPortfolioPermission.get_member_permission_display(roles, additional_permissions) user_managed_domains = model.get("domain_info", []) managed_domains_as_csv = ",".join(user_managed_domains) @@ -231,6 +256,7 @@ class MemberExport(BaseExport): row = [FIELDS.get(column, "") for column in columns] return row + class DomainExport(BaseExport): """ A collection of functions which return csv files regarding Domains. Although class is @@ -1521,4 +1547,3 @@ class DomainRequestDataFull(DomainRequestExport): distinct=True, ) return query - diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 38f00b072..dc6e6ea87 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -20,12 +20,26 @@ Example: permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio, csv_report=True) # Returns same fields but formatted for CSV export """ + from abc import ABC, abstractmethod from registrar.models import ( DomainInvitation, PortfolioInvitation, ) -from django.db.models import CharField, F, ManyToManyField, Q, QuerySet, Value, TextField, OuterRef, Subquery, Func, Case, When +from django.db.models import ( + CharField, + F, + ManyToManyField, + Q, + QuerySet, + Value, + TextField, + OuterRef, + Subquery, + Func, + Case, + When, +) from django.db.models.functions import Concat, Coalesce, Cast from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict @@ -39,9 +53,9 @@ class BaseModelAnnotation(ABC): """ Abstract base class that standardizes how models are annotated for csv exports or complex annotation queries. For example, the Members table / csv export. - + Subclasses define model-specific annotations, filters, and field formatting while inheriting - common queryset building logic. + common queryset building logic. Intended ensure consistent data presentation across both table UI components and CSV exports. """ @@ -118,7 +132,7 @@ class BaseModelAnnotation(ABC): Get a list of fields from related tables. """ return [] - + @classmethod def annotate_and_retrieve_fields( cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs @@ -174,8 +188,7 @@ class BaseModelAnnotation(ABC): model_queryset = ( cls.model() - .objects - .select_related(*select_related) + .objects.select_related(*select_related) .prefetch_related(*prefetch_related) .filter(filter_conditions) .exclude(exclusions) @@ -183,9 +196,7 @@ class BaseModelAnnotation(ABC): .order_by(*sort_fields) .distinct() ) - return cls.annotate_and_retrieve_fields( - model_queryset, annotated_fields, related_table_fields, **kwargs - ) + return cls.annotate_and_retrieve_fields(model_queryset, annotated_fields, related_table_fields, **kwargs) @classmethod def update_queryset(cls, queryset, **kwargs): @@ -193,7 +204,7 @@ class BaseModelAnnotation(ABC): Returns an updated queryset. Override in subclass to update queryset. """ return queryset - + @classmethod def get_model_annotation_dict(cls, **kwargs): return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) @@ -205,6 +216,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): Handles formatting of user details, permissions, and related domain information for both UI display and CSV export. """ + @classmethod def model(cls): # Return the model class that this export handles @@ -247,10 +259,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): if csv_report: domain_query = F("user__permissions__domain__name") last_active_query = Func( - F("user__last_login"), - Value("YYYY-MM-DD"), - function="to_char", - output_field=TextField() + F("user__last_login"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField() ) else: domain_query = Concat( @@ -272,10 +281,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): ), "additional_permissions_display": F("additional_permissions"), "member_display": Case( - When( - Q(user__email__isnull=False) & ~Q(user__email=""), - then=F("user__email") - ), + When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), When( Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), then=Concat( @@ -290,17 +296,12 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): "domain_info": ArrayAgg( domain_query, distinct=True, - filter=Q(user__permissions__domain__isnull=False) + filter=Q(user__permissions__domain__isnull=False) & Q(user__permissions__domain__domain_info__portfolio=portfolio), ), "source": Value("permission", output_field=CharField()), "invitation_date": Coalesce( - Func( - F("invitation__created_at"), - Value("YYYY-MM-DD"), - function="to_char", - output_field=TextField() - ), + Func(F("invitation__created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()), Value("Invalid date"), output_field=TextField(), ), @@ -311,12 +312,16 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): Subquery( LogEntry.objects.filter( content_type=ContentType.objects.get_for_model(PortfolioInvitation), - object_id=Cast(OuterRef("invitation__id"), output_field=TextField()), # Look up the invitation's ID - action_flag=ADDITION - ).order_by("action_time").values("user__email")[:1] + object_id=Cast( + OuterRef("invitation__id"), output_field=TextField() + ), # Look up the invitation's ID + action_flag=ADDITION, + ) + .order_by("action_time") + .values("user__email")[:1] ), Value("Unknown"), - output_field=CharField() + output_field=CharField(), ), } @@ -394,12 +399,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): ), "source": Value("invitation", output_field=CharField()), "invitation_date": Coalesce( - Func( - F("created_at"), - Value("YYYY-MM-DD"), - function="to_char", - output_field=TextField() - ), + Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()), Value("Invalid date"), output_field=TextField(), ), @@ -412,11 +412,13 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): content_type=ContentType.objects.get_for_model(PortfolioInvitation), # Look up the invitation's ID. LogEntry expects a string as this it is stored as json. object_id=Cast(OuterRef("id"), output_field=TextField()), - action_flag=ADDITION - ).order_by("action_time").values("user__email")[:1] + action_flag=ADDITION, + ) + .order_by("action_time") + .values("user__email")[:1] ), Value("Unknown"), - output_field=CharField() + output_field=CharField(), ), } diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 2fe0492d6..ebe537247 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -9,7 +9,10 @@ from django.views import View from registrar.models import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from registrar.utility.model_annotations import PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation +from registrar.utility.model_annotations import ( + PortfolioInvitationModelAnnotation, + UserPortfolioPermissionModelAnnotation, +) from registrar.views.utility.mixins import PortfolioMembersPermission diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 7839d209e..2ad36b71e 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -386,6 +386,21 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View): def get(self, request): """Add additional context data to the template.""" return render(request, "portfolio_members.html") + + def get_context_data(self, **kwargs): + """Add additional context data to the template.""" + + context = super().get_context_data(**kwargs) + portfolio = self.request.session.get("portfolio") + user_count = portfolio.portfolio_users.count() + invitation_count = PortfolioInvitation.objects.filter( + portfolio=portfolio + ).count() + context["member_count"] = user_count + invitation_count + + # check if any portfolio invitations exist 4 portfolio + # check if any userportfolioroles exist 4 portfolio + return context class NewMemberView(PortfolioMembersPermissionView, FormMixin): From 9565731a933b915fae19abc9c76fbb14433941f5 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 15 Nov 2024 16:56:15 -0500 Subject: [PATCH 050/148] trigger PR --- src/registrar/fixtures/fixtures_users.py | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index e60be9872..343686028 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -23,6 +23,13 @@ class UserFixture: """ ADMINS = [ + # { + # "username": "a6b5dd22-f54e-4d55-83ce-ca7b65b2dc1a", + # "first_name": "Rach test admin", + # "last_name": "Mrad test admin", + # "email": "rachid.mrad+2@gmail.com", + # "title": "Super admin tester", + # }, { "username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf", "first_name": "Aditi", @@ -154,6 +161,14 @@ class UserFixture: ] STAFF = [ + + # { + # "username": "994b7a90-f1d1-4140-a3d2-ff34183c7ee2", + # "first_name": "Rach test staff", + # "last_name": "Mrad test staff", + # "email": "rachid.mrad+3@gmail.com", + # "title": "Super staff tester", + # }, { "username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54", "first_name": "Aditi-Analyst", @@ -309,6 +324,18 @@ class UserFixture: # Get all users to be updated (both new and existing) created_or_existing_users = User.objects.filter(username__in=[user.get("username") for user in users]) + # Update `is_staff` for existing users if necessary + users_to_update = [] + for user in created_or_existing_users: + if not user.is_staff: + user.is_staff = True + users_to_update.append(user) + + # Save any users that were updated + if users_to_update: + User.objects.bulk_update(users_to_update, ["is_staff"]) + logger.info(f"Updated {len(users_to_update)} existing users to have is_staff=True.") + # Filter out users who are already in the group users_not_in_group = created_or_existing_users.exclude(groups__id=group.id) From d032d76c7507a4c1be13ecdb03ff9c025593b3aa Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 18 Nov 2024 12:19:47 -0500 Subject: [PATCH 051/148] unique requested domains, portfolio appropriate suborg --- src/registrar/fixtures/fixtures_requests.py | 32 +++++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index f5b57491e..e869cda44 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -6,8 +6,10 @@ from faker import Faker from django.db import transaction from registrar.fixtures.fixtures_portfolios import PortfolioFixture +from registrar.fixtures.fixtures_suborganizations import SuborganizationFixture from registrar.fixtures.fixtures_users import UserFixture from registrar.models import User, DomainRequest, DraftDomain, Contact, Website, FederalAgency +from registrar.models.domain import Domain from registrar.models.portfolio import Portfolio from registrar.models.suborganization import Suborganization @@ -189,7 +191,13 @@ class DomainRequestFixture: if not request.requested_domain: if "requested_domain" in request_dict and request_dict["requested_domain"] is not None: return DraftDomain.objects.get_or_create(name=request_dict["requested_domain"])[0] - return DraftDomain.objects.create(name=cls.fake_dot_gov()) + + # Generate a unique fake domain + # This will help us avoid domain already approved log warnings + while True: + fake_name = cls.fake_dot_gov() + if not Domain.objects.filter(name=fake_name).exists(): + return DraftDomain.objects.create(name=fake_name) return request.requested_domain @classmethod @@ -213,7 +221,7 @@ class DomainRequestFixture: if not request.sub_organization: if "sub_organization" in request_dict and request_dict["sub_organization"] is not None: return Suborganization.objects.get_or_create(name=request_dict["sub_organization"])[0] - return cls._get_random_sub_organization() + return cls._get_random_sub_organization(request) return request.sub_organization @classmethod @@ -226,12 +234,21 @@ class DomainRequestFixture: except Exception as e: logger.warning(f"Expected fixture portfolio, did not find it: {e}") return None - + @classmethod - def _get_random_sub_organization(cls): + def _get_random_sub_organization(cls, request): try: - suborg_options = [Suborganization.objects.first(), Suborganization.objects.last()] - return random.choice(suborg_options) # nosec + # Filter Suborganizations by the request's portfolio + portfolio_suborganizations = Suborganization.objects.filter(portfolio=request.portfolio) + + # Assuming SuborganizationFixture.SUBORGS is a list of dictionaries with a "name" key + suborganization_names = [suborg["name"] for suborg in SuborganizationFixture.SUBORGS] + + # Further filter by names in suborganization_names + suborganization_options = portfolio_suborganizations.filter(name__in=suborganization_names) + + # Randomly choose one if any exist + return random.choice(suborganization_options) if suborganization_options.exists() else None # nosec except Exception as e: logger.warning(f"Expected fixture sub_organization, did not find it: {e}") return None @@ -273,6 +290,9 @@ class DomainRequestFixture: # Lumped under .atomic to ensure we don't make redundant DB calls. # This bundles them all together, and then saves it in a single call. + # The atomic block will cause the code to stop executing if one instance in the + # nested iteration fails, which will cause an early exit and make it hard to debug. + # Comment out with transaction.atomic() when debugging. with transaction.atomic(): try: # Get the usernames of users created in the UserFixture From 55ce772b1518deb59a8b8b0e7f45895a510e3594 Mon Sep 17 00:00:00 2001 From: Matthew Spence Date: Mon, 18 Nov 2024 12:05:40 -0600 Subject: [PATCH 052/148] self host select2 --- src/registrar/assets/css/select2.min.css | 1 + src/registrar/assets/js/select2.min.js | 2 ++ src/registrar/config/settings.py | 2 -- src/registrar/templates/admin/transfer_user.html | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 src/registrar/assets/css/select2.min.css create mode 100644 src/registrar/assets/js/select2.min.js diff --git a/src/registrar/assets/css/select2.min.css b/src/registrar/assets/css/select2.min.css new file mode 100644 index 000000000..f00f15472 --- /dev/null +++ b/src/registrar/assets/css/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline;list-style:none;padding:0}.select2-container .select2-selection--multiple .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;margin-left:5px;padding:0;max-width:100%;resize:none;height:18px;vertical-align:bottom;font-family:sans-serif;overflow:hidden;word-break:keep-all}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option--selectable{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px;padding-right:0px}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;padding-bottom:5px;padding-right:5px;position:relative}.select2-container--default .select2-selection--multiple.select2-selection--clearable{padding-right:25px}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;font-weight:bold;height:20px;margin-right:10px;margin-top:5px;position:absolute;right:0;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:inline-block;margin-left:5px;margin-top:5px;padding:0;padding-left:20px;position:relative;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap}.select2-container--default .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-right:1px solid #aaa;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#999;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px;position:absolute;left:0;top:0}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus{background-color:#f1f1f1;color:#333;outline:none}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-left:1px solid #aaa;border-right:none;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__clear{float:left;margin-left:10px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--group{padding:0}.select2-container--default .select2-results__option--disabled{color:#999}.select2-container--default .select2-results__option--selected{background-color:#ddd}.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0;padding-bottom:5px;padding-right:5px}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;display:inline-block;margin-left:5px;margin-top:5px;padding:0}.select2-container--classic .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#888;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555;outline:none}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option--group{padding:0}.select2-container--classic .select2-results__option--disabled{color:grey}.select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} \ No newline at end of file diff --git a/src/registrar/assets/js/select2.min.js b/src/registrar/assets/js/select2.min.js new file mode 100644 index 000000000..cc9a83f1e --- /dev/null +++ b/src/registrar/assets/js/select2.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.1.0-rc.0 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(t){var e,n,s,p,r,o,h,f,g,m,y,v,i,a,_,s=((u=t&&t.fn&&t.fn.select2&&t.fn.select2.amd?t.fn.select2.amd:u)&&u.requirejs||(u?n=u:u={},g={},m={},y={},v={},i=Object.prototype.hasOwnProperty,a=[].slice,_=/\.js$/,h=function(e,t){var n,s,i=c(e),r=i[0],t=t[1];return e=i[1],r&&(n=x(r=l(r,t))),r?e=n&&n.normalize?n.normalize(e,(s=t,function(e){return l(e,s)})):l(e,t):(r=(i=c(e=l(e,t)))[0],e=i[1],r&&(n=x(r))),{f:r?r+"!"+e:e,n:e,pr:r,p:n}},f={require:function(e){return w(e)},exports:function(e){var t=g[e];return void 0!==t?t:g[e]={}},module:function(e){return{id:e,uri:"",exports:g[e],config:(t=e,function(){return y&&y.config&&y.config[t]||{}})};var t}},r=function(e,t,n,s){var i,r,o,a,l,c=[],u=typeof n,d=A(s=s||e);if("undefined"==u||"function"==u){for(t=!t.length&&n.length?["require","exports","module"]:t,a=0;a":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},s.__cache={};var n=0;return s.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null!=t||(t=e.id?"select2-data-"+e.id:"select2-data-"+(++n).toString()+"-"+s.generateChars(4),e.setAttribute("data-select2-id",t)),t},s.StoreData=function(e,t,n){e=s.GetUniqueElementId(e);s.__cache[e]||(s.__cache[e]={}),s.__cache[e][t]=n},s.GetData=function(e,t){var n=s.GetUniqueElementId(e);return t?s.__cache[n]&&null!=s.__cache[n][t]?s.__cache[n][t]:r(e).data(t):s.__cache[n]},s.RemoveData=function(e){var t=s.GetUniqueElementId(e);null!=s.__cache[t]&&delete s.__cache[t],e.removeAttribute("data-select2-id")},s.copyNonInternalCssClasses=function(e,t){var n=(n=e.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0===e.indexOf("select2-")}),t=(t=t.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0!==e.indexOf("select2-")}),t=n.concat(t);e.setAttribute("class",t.join(" "))},s}),u.define("select2/results",["jquery","./utils"],function(d,p){function s(e,t,n){this.$element=e,this.data=n,this.options=t,s.__super__.constructor.call(this)}return p.Extend(s,p.Observable),s.prototype.render=function(){var e=d('
    ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},s.prototype.clear=function(){this.$results.empty()},s.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=d(''),s=this.options.get("translations").get(e.message);n.append(t(s(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},s.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},s.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested",role:"none"});i.append(l),o.append(a),o.append(i)}else this.template(e,t);return p.StoreData(t,"data",e),t},s.prototype.bind=function(t,e){var i=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){i.clear(),i.append(e.data),t.isOpen()&&(i.setClasses(),i.highlightFirstItem())}),t.on("results:append",function(e){i.append(e.data),t.isOpen()&&i.setClasses()}),t.on("query",function(e){i.hideMessages(),i.showLoading(e)}),t.on("select",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("open",function(){i.$results.attr("aria-expanded","true"),i.$results.attr("aria-hidden","false"),i.setClasses(),i.ensureHighlightVisible()}),t.on("close",function(){i.$results.attr("aria-expanded","false"),i.$results.attr("aria-hidden","true"),i.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=i.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e,t=i.getHighlightedResults();0!==t.length&&(e=p.GetData(t[0],"data"),t.hasClass("select2-results__option--selected")?i.trigger("close",{}):i.trigger("select",{data:e}))}),t.on("results:previous",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t);s<=0||(e=s-1,0===t.length&&(e=0),(s=n.eq(e)).trigger("mouseenter"),t=i.$results.offset().top,n=s.offset().top,s=i.$results.scrollTop()+(n-t),0===e?i.$results.scrollTop(0):n-t<0&&i.$results.scrollTop(s))}),t.on("results:next",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t)+1;s>=n.length||((e=n.eq(s)).trigger("mouseenter"),t=i.$results.offset().top+i.$results.outerHeight(!1),n=e.offset().top+e.outerHeight(!1),e=i.$results.scrollTop()+n-t,0===s?i.$results.scrollTop(0):tthis.$results.outerHeight()||s<0)&&this.$results.scrollTop(n))},s.prototype.template=function(e,t){var n=this.options.get("templateResult"),s=this.options.get("escapeMarkup"),e=n(e,t);null==e?t.style.display="none":"string"==typeof e?t.innerHTML=s(e):d(t).append(e)},s}),u.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),u.define("select2/selection/base",["jquery","../utils","../keys"],function(n,s,i){function r(e,t){this.$element=e,this.options=t,r.__super__.constructor.call(this)}return s.Extend(r,s.Observable),r.prototype.render=function(){var e=n('');return this._tabindex=0,null!=s.GetData(this.$element[0],"old-tabindex")?this._tabindex=s.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},r.prototype.bind=function(e,t){var n=this,s=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",s),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},r.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},r.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&s.GetData(this,"element").select2("close")})})},r.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},r.prototype.position=function(e,t){t.find(".selection").append(e)},r.prototype.destroy=function(){this._detachCloseHandler(this.container)},r.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},r.prototype.isEnabled=function(){return!this.isDisabled()},r.prototype.isDisabled=function(){return this.options.get("disabled")},r}),u.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,s){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e[0].classList.add("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var s=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",s),this.$selection.attr("aria-controls",s),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){var t,n;0!==e.length?(n=e[0],t=this.$selection.find(".select2-selection__rendered"),e=this.display(n,t),t.empty().append(e),(n=n.title||n.text)?t.attr("title",n):t.removeAttr("title")):this.clear()},i}),u.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,c){function r(e,t){r.__super__.constructor.apply(this,arguments)}return c.Extend(r,e),r.prototype.render=function(){var e=r.__super__.render.call(this);return e[0].classList.add("select2-selection--multiple"),e.html('
      '),e},r.prototype.bind=function(e,t){var n=this;r.__super__.bind.apply(this,arguments);var s=e.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s),this.$selection.on("click",function(e){n.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){var t;n.isDisabled()||(t=i(this).parent(),t=c.GetData(t[0],"data"),n.trigger("unselect",{originalEvent:e,data:t}))}),this.$selection.on("keydown",".select2-selection__choice__remove",function(e){n.isDisabled()||e.stopPropagation()})},r.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},r.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},r.prototype.selectionContainer=function(){return i('
    • ')},r.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=this.$selection.find(".select2-selection__rendered").attr("id")+"-choice-",s=0;s')).attr("title",s()),e.attr("aria-label",s()),e.attr("aria-describedby",n),a.StoreData(e[0],"data",t),this.$selection.prepend(e),this.$selection[0].classList.add("select2-selection--clearable"))},e}),u.define("select2/selection/search",["jquery","../utils","../keys"],function(s,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=this.options.get("translations").get("search"),n=s('');this.$searchContainer=n,this.$search=n.find("textarea"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",t());e=e.call(this);return this._transferTabIndex(),e.append(this.$searchContainer),e},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results",r=t.id+"-container";e.call(this,t,n),s.$search.attr("aria-describedby",r),t.on("open",function(){s.$search.attr("aria-controls",i),s.$search.trigger("focus")}),t.on("close",function(){s.$search.val(""),s.resizeSearch(),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.trigger("focus")}),t.on("enable",function(){s.$search.prop("disabled",!1),s._transferTabIndex()}),t.on("disable",function(){s.$search.prop("disabled",!0)}),t.on("focus",function(e){s.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){s.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){s._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){var t;e.stopPropagation(),s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented(),e.which!==l.BACKSPACE||""!==s.$search.val()||0<(t=s.$selection.find(".select2-selection__choice").last()).length&&(t=a.GetData(t[0],"data"),s.searchRemoveChoice(t),e.preventDefault())}),this.$selection.on("click",".select2-search--inline",function(e){s.$search.val()&&e.stopPropagation()});var t=document.documentMode,o=t&&t<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){o?s.$selection.off("input.search input.searchcheck"):s.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){var t;o&&"input"===e.type?s.$selection.off("input.search input.searchcheck"):(t=e.which)!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&s.handleSearch(e)})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){var e;this.resizeSearch(),this._keyUpPrevented||(e=this.$search.val(),this.trigger("query",{term:e})),this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="100%";""===this.$search.attr("placeholder")&&(e=.75*(this.$search.val().length+1)+"em"),this.$search.css("width",e)},e}),u.define("select2/selection/selectionCss",["../utils"],function(n){function e(){}return e.prototype.render=function(e){var t=e.call(this),e=this.options.get("selectionCssClass")||"";return-1!==e.indexOf(":all:")&&(e=e.replace(":all:",""),n.copyNonInternalCssClasses(t[0],this.$element[0])),t.addClass(e),t},e}),u.define("select2/selection/eventRelay",["jquery"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var s=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],r=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){var n;-1!==i.indexOf(e)&&(t=t||{},n=o.Event("select2:"+e,{params:t}),s.$element.trigger(n),-1!==r.indexOf(e)&&(t.prevented=n.isDefaultPrevented()))})},e}),u.define("select2/translation",["jquery","require"],function(t,n){function s(e){this.dict=e||{}}return s.prototype.all=function(){return this.dict},s.prototype.get=function(e){return this.dict[e]},s.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},s._cache={},s.loadPath=function(e){var t;return e in s._cache||(t=n(e),s._cache[e]=t),new s(s._cache[e])},s}),u.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),u.define("select2/data/base",["../utils"],function(n){function s(e,t){s.__super__.constructor.call(this)}return n.Extend(s,n.Observable),s.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},s.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},s.prototype.bind=function(e,t){},s.prototype.destroy=function(){},s.prototype.generateResultId=function(e,t){e=e.id+"-result-";return e+=n.generateChars(4),null!=t.id?e+="-"+t.id.toString():e+="-"+n.generateChars(4),e},s}),u.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var t=this;e(Array.prototype.map.call(this.$element[0].querySelectorAll(":checked"),function(e){return t.item(l(e))}))},n.prototype.select=function(i){var e,r=this;if(i.selected=!0,null!=i.element&&"option"===i.element.tagName.toLowerCase())return i.element.selected=!0,void this.$element.trigger("input").trigger("change");this.$element.prop("multiple")?this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;nthis.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),u.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("select",function(){s._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var s=this;this._checkIfMaximumSelected(function(){e.call(s,t,n)})},e.prototype._checkIfMaximumSelected=function(e,t){var n=this;this.current(function(e){e=null!=e?e.length:0;0=n.maximumSelectionLength?n.trigger("results:message",{message:"maximumSelected",args:{maximum:n.maximumSelectionLength}}):t&&t()})},e}),u.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),u.define("select2/dropdown/search",["jquery"],function(r){function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("translations").get("search"),e=r('');return this.$searchContainer=e,this.$search=e.find("input"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",n()),t.prepend(e),t},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){r(this).off("keyup")}),this.$search.on("keyup input",function(e){s.handleSearch(e)}),t.on("open",function(){s.$search.attr("tabindex",0),s.$search.attr("aria-controls",i),s.$search.trigger("focus"),window.setTimeout(function(){s.$search.trigger("focus")},0)}),t.on("close",function(){s.$search.attr("tabindex",-1),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.val(""),s.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||s.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(s.showSearch(e)?s.$searchContainer[0].classList.remove("select2-search--hide"):s.$searchContainer[0].classList.add("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")})},e.prototype.handleSearch=function(e){var t;this._keyUpPrevented||(t=this.$search.val(),this.trigger("query",{term:t})),this._keyUpPrevented=!1},e.prototype.showSearch=function(e,t){return!0},e}),u.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,s){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,s)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return t="string"==typeof t?{id:"",text:t}:t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),s=t.length-1;0<=s;s--){var i=t[s];this.placeholder.id===i.id&&n.splice(s,1)}return n},e}),u.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,s){this.lastParams={},e.call(this,t,n,s),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("query",function(e){s.lastParams=e,s.loading=!0}),t.on("query:append",function(e){s.lastParams=e,s.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);!this.loading&&e&&(e=this.$results.offset().top+this.$results.outerHeight(!1),this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=e+50&&this.loadMore())},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
    • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),u.define("select2/dropdown/attachBody",["jquery","../utils"],function(u,o){function e(e,t,n){this.$dropdownParent=u(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("open",function(){s._showDropdown(),s._attachPositioningHandler(t),s._bindContainerResultHandlers(t)}),t.on("close",function(){s._hideDropdown(),s._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t[0].classList.remove("select2"),t[0].classList.add("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=u(""),e=e.call(this);return t.append(e),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){var n;this._containerResultsHandlersBound||(n=this,t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0)},e.prototype._attachPositioningHandler=function(e,t){var n=this,s="scroll.select2."+t.id,i="resize.select2."+t.id,r="orientationchange.select2."+t.id,t=this.$container.parents().filter(o.hasScroll);t.each(function(){o.StoreData(this,"select2-scroll-position",{x:u(this).scrollLeft(),y:u(this).scrollTop()})}),t.on(s,function(e){var t=o.GetData(this,"select2-scroll-position");u(this).scrollTop(t.y)}),u(window).on(s+" "+i+" "+r,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,s="resize.select2."+t.id,t="orientationchange.select2."+t.id;this.$container.parents().filter(o.hasScroll).off(n),u(window).off(n+" "+s+" "+t)},e.prototype._positionDropdown=function(){var e=u(window),t=this.$dropdown[0].classList.contains("select2-dropdown--above"),n=this.$dropdown[0].classList.contains("select2-dropdown--below"),s=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var r={height:this.$container.outerHeight(!1)};r.top=i.top,r.bottom=i.top+r.height;var o=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+o,a={left:i.left,top:r.bottom},l=this.$dropdownParent;"static"===l.css("position")&&(l=l.offsetParent());i={top:0,left:0};(u.contains(document.body,l[0])||l[0].isConnected)&&(i=l.offset()),a.top-=i.top,a.left-=i.left,t||n||(s="below"),e||!c||t?!c&&e&&t&&(s="below"):s="above",("above"==s||t&&"below"!==s)&&(a.top=r.top-i.top-o),null!=s&&(this.$dropdown[0].classList.remove("select2-dropdown--below"),this.$dropdown[0].classList.remove("select2-dropdown--above"),this.$dropdown[0].classList.add("select2-dropdown--"+s),this.$container[0].classList.remove("select2-container--below"),this.$container[0].classList.remove("select2-container--above"),this.$container[0].classList.add("select2-container--"+s)),this.$dropdownContainer.css(a)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),u.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,s){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,s)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,s=0;s');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container[0].classList.add("select2-container--"+this.options.get("theme")),r.StoreData(e[0],"element",this.$element),e},o}),u.define("jquery-mousewheel",["jquery"],function(e){return e}),u.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,r,t,o){var a;return null==i.fn.select2&&(a=["open","close","destroy"],i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new r(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,s=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=o.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,s)}),-1 - + - + {% endblock %} {% block breadcrumbs %} From 3b378d3c81fbcf53753067260343ebecda8279d1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:12:43 -0700 Subject: [PATCH 053/148] Cleanup --- src/registrar/admin.py | 11 ++- ...te_user_portfolio_permission_invitation.py | 13 +++- src/registrar/utility/model_annotations.py | 68 ++++++++++--------- src/registrar/views/portfolio_members_json.py | 5 +- src/registrar/views/portfolios.py | 28 ++++---- 5 files changed, 69 insertions(+), 56 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 2d05e760b..1679eeed2 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1268,6 +1268,15 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin): _meta = Meta() + # Question for reviewers: should this include the invitation field? + # This is the same layout as before. + fieldsets = ( + ( + None, + {"fields": ("user", "portfolio", "invitation", "roles", "additional_permissions")}, + ), + ) + # Columns list_display = [ "user", @@ -1275,8 +1284,6 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin): "get_roles", ] - readonly_fields = ["invitation"] - autocomplete_fields = ["user", "portfolio"] search_fields = ["user__first_name", "user__last_name", "user__email", "portfolio__organization_name"] search_help_text = "Search by first name, last name, email, or portfolio." diff --git a/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py b/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py index 6e72792ac..7a73f7710 100644 --- a/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py +++ b/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py @@ -13,15 +13,22 @@ class Command(BaseCommand, PopulateScriptTemplate): def handle(self, **kwargs): """Loops through each DomainRequest object and populates its last_status_update and first_submitted_date values""" - self.existing_invitations = PortfolioInvitation.objects.filter(portfolio__isnull=False, email__isnull=False).select_related('portfolio') + self.existing_invitations = PortfolioInvitation.objects.filter( + portfolio__isnull=False, email__isnull=False + ).select_related("portfolio") filter_condition = {"invitation__isnull": True, "portfolio__isnull": False, "user__email__isnull": False} self.mass_update_records(UserPortfolioPermission, filter_condition, fields_to_update=["invitation"]) def update_record(self, record: UserPortfolioPermission): """Associate the invitation to the right object""" - record.invitation = self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).first() + record.invitation = self.existing_invitations.filter( + email=record.user.email, portfolio=record.portfolio + ).first() TerminalHelper.colorful_logger("INFO", "OKCYAN", f"{TerminalColors.OKCYAN}Adding invitation to {record}") def should_skip_record(self, record) -> bool: """There is nothing to add if no invitation exists""" - return not record or not self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).exists() + return ( + not record + or not self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).exists() + ) diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index dc6e6ea87..105296855 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -39,8 +39,10 @@ from django.db.models import ( Func, Case, When, + Exists, ) from django.db.models.functions import Concat, Coalesce, Cast +from registrar.models.user_group import UserGroup from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.models.utility.orm_helper import ArrayRemove @@ -305,23 +307,8 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): Value("Invalid date"), output_field=TextField(), ), - # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. - # Grab the invitation creator from the audit log. This will need to be replaced with a creator field. - # When that happens, just replace this with F("invitation__creator") - "invited_by": Coalesce( - Subquery( - LogEntry.objects.filter( - content_type=ContentType.objects.get_for_model(PortfolioInvitation), - object_id=Cast( - OuterRef("invitation__id"), output_field=TextField() - ), # Look up the invitation's ID - action_flag=ADDITION, - ) - .order_by("action_time") - .values("user__email")[:1] - ), - Value("Unknown"), - output_field=CharField(), + "invited_by": PortfolioInvitationModelAnnotation.get_invited_by_from_audit_log_query( + object_id_query=Cast(OuterRef("invitation__id"), output_field=TextField()) ), } @@ -362,6 +349,37 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): # Get all members on this portfolio return Q(portfolio=portfolio) + @classmethod + def get_invited_by_from_audit_log_query(cls, object_id_query): + return Coalesce( + Subquery( + LogEntry.objects.filter( + content_type=ContentType.objects.get_for_model(PortfolioInvitation), + object_id=object_id_query, + action_flag=ADDITION, + ) + .annotate( + display_email=Case( + When( + Exists( + UserGroup.objects.filter( + name__in=["cisa_analysts_group", "full_access_group"], + user=OuterRef("user"), + ) + ), + then=Value("help@get.gov") + ), + default=F("user__email"), + output_field=CharField() + ) + ) + .order_by("action_time") + .values("display_email") + ), + Value("Unknown"), + output_field=CharField(), + ) + @classmethod def get_annotated_fields(cls, portfolio, csv_report=False): """ @@ -383,7 +401,6 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): email=OuterRef("email"), # Check if email matches the OuterRef("email") domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio ).annotate(domain_info=domain_query) - return { "first_name": Value(None, output_field=CharField()), "last_name": Value(None, output_field=CharField()), @@ -406,19 +423,8 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. # Grab the invitation creator from the audit log. This will need to be replaced with a creator field. # When that happens, just replace this with F("invitation__creator") - "invited_by": Coalesce( - Subquery( - LogEntry.objects.filter( - content_type=ContentType.objects.get_for_model(PortfolioInvitation), - # Look up the invitation's ID. LogEntry expects a string as this it is stored as json. - object_id=Cast(OuterRef("id"), output_field=TextField()), - action_flag=ADDITION, - ) - .order_by("action_time") - .values("user__email")[:1] - ), - Value("Unknown"), - output_field=CharField(), + "invited_by": cls.get_invited_by_from_audit_log_query( + object_id_query=Cast(OuterRef("id"), output_field=TextField()) ), } diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index ebe537247..3bf761858 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -1,9 +1,6 @@ from django.http import JsonResponse from django.core.paginator import Paginator -from django.db.models import Value, F, CharField, TextField, Q, Case, When, OuterRef, Subquery -from django.db.models.expressions import Func -from django.db.models.functions import Cast, Coalesce, Concat -from django.contrib.postgres.aggregates import ArrayAgg +from django.db.models import F, Q from django.urls import reverse from django.views import View diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 2ad36b71e..09ff159c5 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -385,22 +385,18 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View): def get(self, request): """Add additional context data to the template.""" - return render(request, "portfolio_members.html") - - def get_context_data(self, **kwargs): - """Add additional context data to the template.""" - - context = super().get_context_data(**kwargs) - portfolio = self.request.session.get("portfolio") - user_count = portfolio.portfolio_users.count() - invitation_count = PortfolioInvitation.objects.filter( - portfolio=portfolio - ).count() - context["member_count"] = user_count + invitation_count - - # check if any portfolio invitations exist 4 portfolio - # check if any userportfolioroles exist 4 portfolio - return context + # Get portfolio from session + portfolio = request.session.get("portfolio") + context = {} + if portfolio: + user_count = portfolio.portfolio_users.count() + invitation_count = PortfolioInvitation.objects.filter( + portfolio=portfolio + ).count() + context.update({ + "member_count": user_count + invitation_count + }) + return render(request, "portfolio_members.html", context=context) class NewMemberView(PortfolioMembersPermissionView, FormMixin): From 8f18a409ea8243f53fc9f26177f2a43d844138f4 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 18 Nov 2024 13:51:39 -0500 Subject: [PATCH 054/148] added logic to not display blue info box in certain condition, and added test --- src/registrar/templates/domain_detail.html | 30 ++++++++++++---------- src/registrar/tests/test_views_domain.py | 21 +++++++++++++++ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e2c2d0129..36025bc31 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -58,22 +58,24 @@ {% include "includes/domain_dates.html" %} - {% if is_portfolio_admin and not is_domain_manager %} -
      -
      -

      - To manage information for this domain, you must add yourself as a domain manager. -

      + {% if analyst_action != 'edit' or analyst_action_location != domain.pk %} + {% if is_portfolio_admin and not is_domain_manager and not is_analyst_managing_domain %} +
      +
      +

      + To manage information for this domain, you must add yourself as a domain manager. +

      +
      -
      - {% elif is_portfolio_user and not is_domain_manager %} -
      -
      -

      - You don't have access to manage {{domain.name}}. If you need to make updates, contact one of the listed domain managers. -

      + {% elif is_portfolio_user and not is_domain_manager and not is_analyst_managing_domain %} +
      +
      +

      + You don't have access to manage {{domain.name}}. If you need to make updates, contact one of the listed domain managers. +

      +
      -
      + {% endif %} {% endif %} diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index fcb03deb4..fc7df00b1 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -323,6 +323,27 @@ class TestDomainDetail(TestDomainOverview): self.assertContains(detail_page, "noinformation.gov") self.assertContains(detail_page, "Domain missing domain information") + def test_domain_detail_with_analyst_managing_domain(self): + """Test that domain management page returns 200 and does not display + blue error message when an analyst is managing the domain""" + with less_console_noise(): + # have to use staff user for this test + staff_user = create_user() + # staff_user.save() + self.client.force_login(staff_user) + + # need to set the analyst_action and analyst_action_location + # in the session to emulate user clicking Manage Domain + # in the admin interface + session = self.client.session + session["analyst_action"] = "edit" + session["analyst_action_location"] = self.domain.id + session.save() + + detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) + + self.assertNotContains(detail_page, "To manage information for this domain, you must add yourself as a domain manager.") + @less_console_noise_decorator @override_flag("organization_feature", active=True) def test_domain_readonly_on_detail_page(self): From 051efe798d798f38f585b0c676237c8040874ccc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 18 Nov 2024 14:07:34 -0500 Subject: [PATCH 055/148] oops undo accidental commit --- src/registrar/templates/domain_detail.html | 31 ++++++++++------------ src/registrar/tests/test_views_domain.py | 21 --------------- 2 files changed, 14 insertions(+), 38 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 36025bc31..f322e501e 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -58,27 +58,24 @@ {% include "includes/domain_dates.html" %} - {% if analyst_action != 'edit' or analyst_action_location != domain.pk %} - {% if is_portfolio_admin and not is_domain_manager and not is_analyst_managing_domain %} -
      -
      -

      - To manage information for this domain, you must add yourself as a domain manager. -

      -
      + {% if is_portfolio_admin and not is_domain_manager %} +
      +
      +

      + To manage information for this domain, you must add yourself as a domain manager. +

      - {% elif is_portfolio_user and not is_domain_manager and not is_analyst_managing_domain %} -
      -
      -

      - You don't have access to manage {{domain.name}}. If you need to make updates, contact one of the listed domain managers. -

      -
      +
      + {% elif is_portfolio_user and not is_domain_manager %} +
      +
      +

      + You don't have access to manage {{domain.name}}. If you need to make updates, contact one of the listed domain managers. +

      - {% endif %} +
      {% endif %} - {% url 'domain-dns-nameservers' pk=domain.id as url %} {% if domain.nameservers|length > 0 %} {% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=is_editable %} diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index fc7df00b1..fcb03deb4 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -323,27 +323,6 @@ class TestDomainDetail(TestDomainOverview): self.assertContains(detail_page, "noinformation.gov") self.assertContains(detail_page, "Domain missing domain information") - def test_domain_detail_with_analyst_managing_domain(self): - """Test that domain management page returns 200 and does not display - blue error message when an analyst is managing the domain""" - with less_console_noise(): - # have to use staff user for this test - staff_user = create_user() - # staff_user.save() - self.client.force_login(staff_user) - - # need to set the analyst_action and analyst_action_location - # in the session to emulate user clicking Manage Domain - # in the admin interface - session = self.client.session - session["analyst_action"] = "edit" - session["analyst_action_location"] = self.domain.id - session.save() - - detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) - - self.assertNotContains(detail_page, "To manage information for this domain, you must add yourself as a domain manager.") - @less_console_noise_decorator @override_flag("organization_feature", active=True) def test_domain_readonly_on_detail_page(self): From 5d1e8e6e84dcafa68a7e06d3f95646e8ee97dc3b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 18 Nov 2024 14:25:59 -0500 Subject: [PATCH 056/148] wip --- src/gulpfile.js | 61 +- src/package-lock.json | 5582 ++++++++++++++++++++++------ src/package.json | 8 +- src/registrar/assets/js/get-gov.js | 2823 +------------- 4 files changed, 4586 insertions(+), 3888 deletions(-) diff --git a/src/gulpfile.js b/src/gulpfile.js index c8d5ca261..3f54ce829 100644 --- a/src/gulpfile.js +++ b/src/gulpfile.js @@ -1,21 +1,23 @@ /* gulpfile.js */ +const gulp = require('gulp'); +const webpack = require('webpack-stream'); const uswds = require('@uswds/compile'); +const ASSETS_DIR = './registrar/assets/'; +const JS_MODULES_SRC = ASSETS_DIR + 'js/modules/*.js'; +const JS_BUNDLE_DEST = ASSETS_DIR + 'js'; + /** * USWDS version * Set the version of USWDS you're using (2 or 3) */ - uswds.settings.version = 3; /** * Path settings * Set as many as you need */ - -const ASSETS_DIR = './registrar/assets/'; - uswds.paths.dist.css = ASSETS_DIR + 'css'; uswds.paths.dist.sass = ASSETS_DIR + 'sass'; uswds.paths.dist.theme = ASSETS_DIR + 'sass/_theme'; @@ -23,15 +25,58 @@ uswds.paths.dist.fonts = ASSETS_DIR + 'fonts'; uswds.paths.dist.js = ASSETS_DIR + 'js'; uswds.paths.dist.img = ASSETS_DIR + 'img'; +/** + * Task: Bundle JavaScript modules using Webpack + */ +gulp.task('bundle-js', () => { + return gulp + .src(JS_MODULES_SRC) + .pipe( + webpack({ + mode: 'production', // Use 'development' for debugging + output: { + filename: 'get-gov.js', + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'], + }, + }, + }, + ], + }, + }) + ) + .pipe(gulp.dest(JS_BUNDLE_DEST)); +}); + +/** + * Task: Watch for changes in JavaScript modules + */ +gulp.task('watch-js', () => { + gulp.watch(JS_MODULES_SRC, gulp.series('bundle-js')); +}); + +/** + * Combine all watch tasks + */ +gulp.task('watch', gulp.parallel('watch-js', uswds.watch)); + /** * Exports * Add as many as you need + * Some tasks combine USWDS compilation and JavaScript precompilation. */ - -exports.default = uswds.compile; +exports.default = gulp.series(uswds.compile, 'bundle-js'); exports.init = uswds.init; -exports.compile = uswds.compile; -exports.watch = uswds.watch; +exports.compile = gulp.series(uswds.compile, 'bundle-js'); +exports.watch = gulp.parallel('watch'); exports.copyAssets = uswds.copyAssets exports.updateUswds = uswds.updateUswds \ No newline at end of file diff --git a/src/package-lock.json b/src/package-lock.json index 08e70dd51..a02672d8a 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -14,7 +14,1580 @@ "sass": "^1.54.8" }, "devDependencies": { - "@uswds/compile": "^1.0.0-beta.3" + "@babel/core": "^7.26.0", + "@babel/preset-env": "^7.26.0", + "@uswds/compile": "^1.0.0-beta.3", + "babel-loader": "^9.2.1", + "gulp": "^5.0.0", + "webpack": "^5.96.1", + "webpack-stream": "^7.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "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/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", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "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, + "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==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@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, + "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", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "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==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-simple-access": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@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", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@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-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@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", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@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-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@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-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-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@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-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-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "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==", + "dev": true, + "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", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@bufbuild/protobuf": { @@ -97,6 +1670,85 @@ "node": ">=0.10.0" } }, + "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, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -132,12 +1784,44 @@ "node": ">= 8" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, "node_modules/@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.12.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", @@ -186,6 +1870,938 @@ "sass-embedded": "1.69.5" } }, + "node_modules/@uswds/compile/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/@uswds/compile/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", + "dev": true, + "dependencies": { + "async-done": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", + "dev": true, + "dependencies": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/@uswds/compile/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/@uswds/compile/node_modules/copy-props": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", + "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "dev": true, + "dependencies": { + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" + } + }, + "node_modules/@uswds/compile/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "node_modules/@uswds/compile/node_modules/each-props/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, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/fast-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", + "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", + "dev": true + }, + "node_modules/@uswds/compile/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/fined/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, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/@uswds/compile/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "node_modules/@uswds/compile/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/@uswds/compile/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/glob-watcher": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", + "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "dev": true, + "dependencies": { + "sparkles": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "dev": true, + "dependencies": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", + "dev": true, + "dependencies": { + "glogg": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@uswds/compile/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, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/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, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", + "dev": true, + "dependencies": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "dev": true, + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@uswds/compile/node_modules/liftoff/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, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/micromatch/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, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@uswds/compile/node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "dev": true, + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", + "dev": true, + "dependencies": { + "sver-compat": "^1.5.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "dev": true, + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/undertaker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", + "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "dev": true, + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@uswds/compile/node_modules/vinyl-sourcemap/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uswds/compile/node_modules/y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true + }, + "node_modules/@uswds/compile/node_modules/yargs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", + "dev": true, + "dependencies": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.1" + } + }, + "node_modules/@uswds/compile/node_modules/yargs-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "dev": true, + "dependencies": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + } + }, "node_modules/@uswds/uswds": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.8.1.tgz", @@ -201,6 +2817,164 @@ "node": ">= 4" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, "node_modules/acorn": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", @@ -258,6 +3032,51 @@ "node": ">=8" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", @@ -291,6 +3110,21 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ansi-wrap": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", @@ -301,121 +3135,15 @@ } }, "node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/anymatch/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/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, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/anymatch/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, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/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, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/append-buffer": { @@ -562,6 +3290,15 @@ "node": ">=0.10.0" } }, + "node_modules/array-sort/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -606,18 +3343,17 @@ } }, "node_modules/async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "dev": true, "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/async-each": { @@ -633,15 +3369,15 @@ ] }, "node_modules/async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, "dependencies": { - "async-done": "^1.2.2" + "async-done": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/atob": { @@ -701,24 +3437,83 @@ "node": ">=4" } }, - "node_modules/bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", "dev": true, "dependencies": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", + "dev": true, + "dependencies": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, "node_modules/balanced-match": { @@ -726,6 +3521,13 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "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==", + "dev": true, + "optional": true + }, "node_modules/base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", @@ -803,12 +3605,14 @@ } }, "node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/binaryextensions": { @@ -876,30 +3680,20 @@ } }, "node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -916,10 +3710,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -1032,9 +3826,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001620", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", - "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "dev": true, "funding": [ { @@ -1051,6 +3845,34 @@ } ] }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "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", @@ -1093,48 +3915,26 @@ } }, "node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", - "dev": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/chokidar/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/chownr": { @@ -1142,6 +3942,15 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -1172,35 +3981,14 @@ } }, "node_modules/cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "node_modules/clone": { @@ -1274,6 +4062,24 @@ "node": ">=0.10.0" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "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 + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -1291,6 +4097,12 @@ "node": ">= 6" } }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -1336,13 +4148,29 @@ } }, "node_modules/copy-props": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", - "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", "dev": true, "dependencies": { - "each-props": "^1.3.2", + "each-props": "^3.0.0", "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "browserslist": "^4.24.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, "node_modules/core-util-is": { @@ -1478,6 +4306,15 @@ "node": ">=0.10.0" } }, + "node_modules/default-compare/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-resolution": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", @@ -1654,31 +4491,22 @@ } }, "node_modules/each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "dev": true, "dependencies": { - "is-plain-object": "^2.0.1", + "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0" - } - }, - "node_modules/each-props/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, - "dependencies": { - "isobject": "^3.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" } }, "node_modules/electron-to-chromium": { - "version": "1.4.773", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.773.tgz", - "integrity": "sha512-87eHF+h3PlCRwbxVEAw9KtK3v7lWfc/sUDr0W76955AdYTG4bV/k0zrl585Qnj/skRMH2qOSiE+kqMeOQ+LOpw==", + "version": "1.5.62", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.62.tgz", + "integrity": "sha512-t8c+zLmJHa9dJy96yBZRXGQYoiCEnHYgFwn1asvSPZSUdVxnB62A4RASd7k41ytG3ErFBA0TpHlKg9D9SQBmLg==", "dev": true }, "node_modules/element-closest": { @@ -1689,6 +4517,12 @@ "node": ">=4.0.0" } }, + "node_modules/emoji-regex": { + "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 + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1697,6 +4531,19 @@ "once": "^1.4.0" } }, + "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==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1719,6 +4566,18 @@ "node": ">=4" } }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1749,6 +4608,12 @@ "node": ">= 0.4" } }, + "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==", + "dev": true + }, "node_modules/es5-ext": { "version": "0.10.64", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", @@ -1802,9 +4667,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -1819,6 +4684,19 @@ "node": ">=0.8.0" } }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -1834,6 +4712,45 @@ "node": ">=0.10" } }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -1844,6 +4761,15 @@ "es5-ext": "~0.10.14" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -2015,6 +4941,18 @@ "node": ">= 0.10" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-fifo": { + "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 + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2031,12 +4969,36 @@ "node": ">=8.6.0" } }, - "node_modules/fast-levenshtein": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", - "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2070,18 +5032,115 @@ } }, "node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/find-up": { @@ -2097,151 +5156,43 @@ } }, "node_modules/findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "dev": true, "dependencies": { "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", "resolve-dir": "^1.0.1" }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/findup-sync/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/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, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/findup-sync/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, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/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, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" } }, "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, "dependencies": { "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", + "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/fined/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, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" } }, "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/flush-write-stream": { @@ -2306,16 +5257,16 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "node_modules/fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/fs.realpath": { @@ -2324,22 +5275,16 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", - "dev": true, + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ "darwin" ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, "engines": { - "node": ">= 4.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { @@ -2350,11 +5295,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, "node_modules/get-intrinsic": { "version": "1.2.4", @@ -2429,64 +5386,53 @@ } }, "node_modules/glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", "dev": true, "dependencies": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/glob-stream/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/glob-stream/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, "node_modules/glob-watcher": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", - "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "dev": true, "dependencies": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "normalize-path": "^3.0.0", - "object.defaults": "^1.1.0" + "async-done": "^2.0.0", + "chokidar": "^3.5.3" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/global-modules": { @@ -2519,6 +5465,15 @@ "node": ">=0.10.0" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -2540,15 +5495,15 @@ } }, "node_modules/glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", "dev": true, "dependencies": { - "sparkles": "^1.0.0" + "sparkles": "^2.1.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/gopd": { @@ -2570,53 +5525,47 @@ "dev": true }, "node_modules/gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", "dev": true, "dependencies": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" }, "bin": { "gulp": "bin/gulp.js" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", "dev": true, "dependencies": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" }, "bin": { "gulp": "bin/gulp.js" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/gulp-postcss": { @@ -2717,15 +5666,15 @@ } }, "node_modules/gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", "dev": true, "dependencies": { - "glogg": "^1.0.0" + "glogg": "^2.2.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/has-flag": { @@ -2800,10 +5749,22 @@ "node": ">=0.10.0" } }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { "is-buffer": "^1.1.5" @@ -2908,6 +5869,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2971,12 +5944,12 @@ "dev": true }, "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/invert-kv": { @@ -3028,15 +6001,14 @@ "dev": true }, "node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": { - "binary-extensions": "^1.0.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/is-buffer": { @@ -3046,12 +6018,15 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3100,15 +6075,12 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "version": "3.0.0", + "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, - "dependencies": { - "number-is-nan": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/is-glob": { @@ -3132,27 +6104,11 @@ } }, "node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=0.12.0" } }, "node_modules/is-path-cwd": { @@ -3273,12 +6229,68 @@ "url": "https://bevry.me/fund" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "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, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/just-debounce": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", @@ -3291,10 +6303,13 @@ "integrity": "sha512-NTDqo7XhzL1fqmUzYroiyK2qGua7sOMzLav35BfNA/mPUSCtw8pZghHFMTYR9JdnJ23IQz695FcaM6EE6bpbFQ==" }, "node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, "engines": { "node": ">=0.10.0" } @@ -3308,16 +6323,12 @@ } }, "node_modules/last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", "dev": true, - "dependencies": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/lazystream": { @@ -3345,46 +6356,30 @@ } }, "node_modules/lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", "dev": true, - "dependencies": { - "flush-write-stream": "^1.0.2" - }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", "dev": true, "dependencies": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/liftoff/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, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, "node_modules/lilconfig": { @@ -3412,6 +6407,15 @@ "node": ">=0.10.0" } }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3428,12 +6432,30 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "dev": true + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3511,6 +6533,27 @@ "node": ">= 0.10.0" } }, + "node_modules/matchdep/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/matchdep/node_modules/define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -3524,14 +6567,16 @@ "node": ">=0.10.0" } }, - "node_modules/matchdep/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==", + "node_modules/matchdep/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "dev": true, "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { "node": ">=0.10.0" @@ -3589,6 +6634,30 @@ "node": ">=0.10.0" } }, + "node_modules/matchdep/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/matchdep/node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -3634,6 +6703,32 @@ "node": ">=0.10.0" } }, + "node_modules/matchdep/node_modules/micromatch/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, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/matches-selector": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/matches-selector/-/matches-selector-1.2.0.tgz", @@ -3661,6 +6756,25 @@ "timers-ext": "^0.1.7" } }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3683,49 +6797,25 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/micromatch/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "dependencies": { - "to-regex-range": "^5.0.1" + "mime-db": "1.52.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/micromatch/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/micromatch/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" + "node": ">= 0.6" } }, "node_modules/minimatch": { @@ -3796,18 +6886,18 @@ } }, "node_modules/mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "dev": true, "optional": true }, @@ -3923,6 +7013,12 @@ "node": ">=0.10.0" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -3949,9 +7045,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/node.extend": { @@ -4005,15 +7101,15 @@ } }, "node_modules/now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "dependencies": { - "once": "^1.3.2" + "once": "^1.4.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/nth-check": { @@ -4469,9 +7565,9 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -4711,6 +7807,12 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4741,6 +7843,15 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/puppeteer": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz", @@ -4806,6 +7917,21 @@ } ] }, + "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", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4888,113 +8014,14 @@ } }, "node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=0.10" - } - }, - "node_modules/readdirp/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/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, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/readdirp/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, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/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, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=8.10.0" } }, "node_modules/receptor": { @@ -5009,15 +8036,48 @@ } }, "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "dependencies": { - "resolve": "^1.1.6" + "resolve": "^1.20.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" } }, "node_modules/regex-not": { @@ -5070,6 +8130,41 @@ "node": ">=0.10.0" } }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz", + "integrity": "sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, "node_modules/remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -5131,17 +8226,12 @@ } }, "node_modules/replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/replacestream": { @@ -5164,6 +8254,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -5206,15 +8305,15 @@ "integrity": "sha512-hNS03NEmVpJheF7yfyagNh57XuKc0z+NkSO0oBbeO67o6IJKoqlDfnNIxhjp7aTWwjmSWZQhtiGrOgZXVyM90w==" }, "node_modules/resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", "dev": true, "dependencies": { - "value-or-function": "^3.0.0" + "value-or-function": "^4.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/resolve-url": { @@ -5303,6 +8402,12 @@ "ret": "~0.1.10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/sass": { "version": "1.77.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", @@ -5498,126 +8603,23 @@ "node": ">=14.0.0" } }, - "node_modules/sass/node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "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==", + "dev": true, "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/sass/node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" + "node": ">= 12.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sass/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sass/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/sass/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sass/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/sass/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sass/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/sass/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/semver": { @@ -5635,15 +8637,24 @@ } }, "node_modules/semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, "dependencies": { - "sver-compat": "^1.5.0" + "sver": "^1.8.3" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" } }, "node_modules/set-blocking": { @@ -5853,6 +8864,16 @@ "decode-uri-component": "^0.2.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/source-map-url": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", @@ -5861,12 +8882,12 @@ "dev": true }, "node_modules/sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/spdx-correct": { @@ -5896,9 +8917,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true }, "node_modules/split-string": { @@ -5972,6 +8993,15 @@ "node": ">=0.10.0" } }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" + } + }, "node_modules/stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", @@ -5984,6 +9014,20 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, + "node_modules/streamx": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", + "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -5993,38 +9037,17 @@ } }, "node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/strip-ansi": { @@ -6087,6 +9110,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", + "dev": true, + "optionalDependencies": { + "semver": "^6.3.0" + } + }, "node_modules/sver-compat": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", @@ -6097,6 +9129,25 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/sver/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -6136,6 +9187,140 @@ "node": ">= 6" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "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, + "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, + "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 + }, + "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, + "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, + "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", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true + }, "node_modules/textextensions": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", @@ -6245,16 +9430,14 @@ } }, "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "is-number": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0" } }, "node_modules/to-regex/node_modules/define-property": { @@ -6321,15 +9504,15 @@ } }, "node_modules/to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "dev": true, "dependencies": { - "through2": "^2.0.3" + "streamx": "^2.12.5" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/tr46": { @@ -6379,33 +9562,27 @@ } }, "node_modules/undertaker": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", - "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, "dependencies": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "fast-levenshtein": "^1.0.0", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/undici-types": { @@ -6414,6 +9591,46 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "devOptional": true }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -6498,9 +9715,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "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==", "dev": true, "funding": [ { @@ -6517,8 +9734,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -6527,6 +9744,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -6549,15 +9775,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/validate-npm-package-license": { @@ -6571,12 +9794,12 @@ } }, "node_modules/value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/varint": { @@ -6602,62 +9825,162 @@ "node": ">= 0.10" } }, - "node_modules/vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", "dev": true, "dependencies": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" + "bl": "^5.0.0", + "vinyl": "^3.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/vinyl-contents/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/vinyl-contents/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vinyl-contents/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" } }, "node_modules/vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, "dependencies": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, - "node_modules/vinyl-sourcemap/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "node_modules/vinyl-sourcemap/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/vinyl-sourcemap/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, "dependencies": { - "remove-trailing-separator": "^1.0.1" + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, "node_modules/vinyl-sourcemaps-apply": { @@ -6687,11 +10010,162 @@ "node": ">= 0.10" } }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "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", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-7.0.0.tgz", + "integrity": "sha512-XoAQTHyCaYMo6TS7Atv1HYhtmBgKiVLONJbzLBl2V3eibXQ2IT/MCRM841RW/r3vToKD5ivrTJFWgd/ghoxoRg==", + "dev": true, + "dependencies": { + "fancy-log": "^1.3.3", + "lodash.clone": "^4.3.2", + "lodash.some": "^4.2.2", + "memory-fs": "^0.5.0", + "plugin-error": "^1.0.1", + "supports-color": "^8.1.1", + "through": "^2.3.8", + "vinyl": "^2.2.1" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "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==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/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, + "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/webpack/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, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/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 + }, + "node_modules/webpack/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, + "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/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -6725,37 +10199,20 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, "node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" + "node": ">=10" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -6793,10 +10250,13 @@ } }, "node_modules/y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } }, "node_modules/yallist": { "version": "4.0.0", @@ -6813,24 +10273,21 @@ } }, "node_modules/yargs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", - "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "dependencies": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.1" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, "node_modules/yargs-parser": { @@ -6843,13 +10300,12 @@ } }, "node_modules/yargs/node_modules/yargs-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", - "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, - "dependencies": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" + "engines": { + "node": ">=10" } }, "node_modules/yauzl": { @@ -6860,6 +10316,18 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/src/package.json b/src/package.json index e16bc8198..3cad1b207 100644 --- a/src/package.json +++ b/src/package.json @@ -15,6 +15,12 @@ "sass": "^1.54.8" }, "devDependencies": { - "@uswds/compile": "^1.0.0-beta.3" + "@babel/core": "^7.26.0", + "@babel/preset-env": "^7.26.0", + "@uswds/compile": "^1.0.0-beta.3", + "babel-loader": "^9.2.1", + "gulp": "^5.0.0", + "webpack": "^5.96.1", + "webpack-stream": "^7.0.0" } } diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index adcc21d2a..ff128ce0e 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1,2822 +1 @@ -/** - * @file get-gov.js includes custom code for the .gov registrar. - * - * Constants and helper functions are at the top. - * Event handlers are in the middle. - * Initialization (run-on-load) stuff goes at the bottom. - */ - - -var DEFAULT_ERROR = "Please check this field for errors."; - -var INFORMATIVE = "info"; -var WARNING = "warning"; -var ERROR = "error"; -var SUCCESS = "success"; - -// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> -// Helper functions. - -/** - * Hide element - * -*/ -const hideElement = (element) => { - element.classList.add('display-none'); -}; - -/** -* Show element -* -*/ -const showElement = (element) => { - element.classList.remove('display-none'); -}; - -/** - * Helper function to get the CSRF token from the cookie - * -*/ -function getCsrfToken() { - return document.querySelector('input[name="csrfmiddlewaretoken"]').value; -} - -/** - * Helper function that scrolls to an element - * @param {string} attributeName - The string "class" or "id" - * @param {string} attributeValue - The class or id name - */ -function ScrollToElement(attributeName, attributeValue) { - let targetEl = null; - - if (attributeName === 'class') { - targetEl = document.getElementsByClassName(attributeValue)[0]; - } else if (attributeName === 'id') { - targetEl = document.getElementById(attributeValue); - } else { - console.error('Error: unknown attribute name provided.'); - return; // Exit the function if an invalid attributeName is provided - } - - if (targetEl) { - const rect = targetEl.getBoundingClientRect(); - const scrollTop = window.scrollY || document.documentElement.scrollTop; - window.scrollTo({ - top: rect.top + scrollTop, - behavior: 'smooth' // Optional: for smooth scrolling - }); - } -} - -/** Makes an element invisible. */ -function makeHidden(el) { - el.style.position = "absolute"; - el.style.left = "-100vw"; - // The choice of `visiblity: hidden` - // over `display: none` is due to - // UX: the former will allow CSS - // transitions when the elements appear. - el.style.visibility = "hidden"; -} - -/** Makes visible a perviously hidden element. */ -function makeVisible(el) { - el.style.position = "relative"; - el.style.left = "unset"; - el.style.visibility = "visible"; -} - -/** - * Toggles expand_more / expand_more svgs in buttons or anchors - * @param {Element} element - DOM element - */ -function toggleCaret(element) { - // Get a reference to the use element inside the button - const useElement = element.querySelector('use'); - // Check if the span element text is 'Hide' - if (useElement.getAttribute('xlink:href') === '/public/img/sprite.svg#expand_more') { - // Update the xlink:href attribute to expand_more - useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less'); - } else { - // Update the xlink:href attribute to expand_less - useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more'); - } -} - -/** - * Helper function that scrolls to an element - * @param {string} attributeName - The string "class" or "id" - * @param {string} attributeValue - The class or id name - */ -function ScrollToElement(attributeName, attributeValue) { - let targetEl = null; - - if (attributeName === 'class') { - targetEl = document.getElementsByClassName(attributeValue)[0]; - } else if (attributeName === 'id') { - targetEl = document.getElementById(attributeValue); - } else { - console.error('Error: unknown attribute name provided.'); - return; // Exit the function if an invalid attributeName is provided - } - - if (targetEl) { - const rect = targetEl.getBoundingClientRect(); - const scrollTop = window.scrollY || document.documentElement.scrollTop; - window.scrollTo({ - top: rect.top + scrollTop, - behavior: 'smooth' // Optional: for smooth scrolling - }); - } -} - -/** Creates and returns a live region element. */ -function createLiveRegion(id) { - const liveRegion = document.createElement("div"); - liveRegion.setAttribute("role", "region"); - liveRegion.setAttribute("aria-live", "polite"); - liveRegion.setAttribute("id", id + "-live-region"); - liveRegion.classList.add("usa-sr-only"); - document.body.appendChild(liveRegion); - return liveRegion; -} - -/** Announces changes to assistive technology users. */ -function announce(id, text) { - let liveRegion = document.getElementById(id + "-live-region"); - if (!liveRegion) liveRegion = createLiveRegion(id); - liveRegion.innerHTML = text; -} - -/** - * Slow down event handlers by limiting how frequently they fire. - * - * A wait period must occur with no activity (activity means "this - * debounce function being called") before the handler is invoked. - * - * @param {Function} handler - any JS function - * @param {number} cooldown - the wait period, in milliseconds - */ -function debounce(handler, cooldown=600) { - let timeout; - return function(...args) { - const context = this; - clearTimeout(timeout); - timeout = setTimeout(() => handler.apply(context, args), cooldown); - } -} - -/** Asyncronously fetches JSON. No error handling. */ -function fetchJSON(endpoint, callback, url="/api/v1/") { - const xhr = new XMLHttpRequest(); - xhr.open('GET', url + endpoint); - xhr.send(); - xhr.onload = function() { - if (xhr.status != 200) return; - callback(JSON.parse(xhr.response)); - }; - // nothing, don't care - // xhr.onerror = function() { }; -} - -/** Modifies CSS and HTML when an input is valid/invalid. */ -function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) { - if (valid) { - el.setCustomValidity(""); - el.removeAttribute("aria-invalid"); - el.classList.remove('usa-input--error'); - } else { - el.classList.remove('usa-input--success'); - el.setAttribute("aria-invalid", "true"); - el.setCustomValidity(msg); - el.classList.add('usa-input--error'); - } -} - -/** Display (or hide) a message beneath an element. */ -function inlineToast(el, id, style, msg) { - if (!el.id && !id) { - console.error("Elements must have an `id` to show an inline toast."); - return; - } - let toast = document.getElementById((el.id || id) + "--toast"); - if (style) { - if (!toast) { - // create and insert the message div - toast = document.createElement("div"); - const toastBody = document.createElement("div"); - const p = document.createElement("p"); - toast.setAttribute("id", (el.id || id) + "--toast"); - toast.className = `usa-alert usa-alert--${style} usa-alert--slim`; - toastBody.classList.add("usa-alert__body"); - p.classList.add("usa-alert__text"); - p.innerHTML = msg; - toastBody.appendChild(p); - toast.appendChild(toastBody); - el.parentNode.insertBefore(toast, el.nextSibling); - } else { - // update and show the existing message div - toast.className = `usa-alert usa-alert--${style} usa-alert--slim`; - toast.querySelector("div p").innerHTML = msg; - makeVisible(toast); - } - } else { - if (toast) makeHidden(toast); - } -} - -function checkDomainAvailability(el) { - const callback = (response) => { - toggleInputValidity(el, (response && response.available), msg=response.message); - announce(el.id, response.message); - - // Determines if we ignore the field if it is just blank - ignore_blank = el.classList.contains("blank-ok") - if (el.validity.valid) { - el.classList.add('usa-input--success'); - // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration - inlineToast(el.parentElement, el.id, SUCCESS, response.message); - } else if (ignore_blank && response.code == "required"){ - // Visually remove the error - error = "usa-input--error" - if (el.classList.contains(error)){ - el.classList.remove(error) - } - } else { - inlineToast(el.parentElement, el.id, ERROR, response.message); - } - } - fetchJSON(`available/?domain=${el.value}`, callback); -} - -/** Hides the toast message and clears the aira live region. */ -function clearDomainAvailability(el) { - el.classList.remove('usa-input--success'); - announce(el.id, ""); - // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration - inlineToast(el.parentElement, el.id); -} - -/** Runs all the validators associated with this element. */ -function runValidators(el) { - const attribute = el.getAttribute("validate") || ""; - if (!attribute.length) return; - const validators = attribute.split(" "); - let isInvalid = false; - for (const validator of validators) { - switch (validator) { - case "domain": - checkDomainAvailability(el); - break; - } - } - toggleInputValidity(el, !isInvalid); -} - -/** Clears all the validators associated with this element. */ -function clearValidators(el) { - const attribute = el.getAttribute("validate") || ""; - if (!attribute.length) return; - const validators = attribute.split(" "); - for (const validator of validators) { - switch (validator) { - case "domain": - clearDomainAvailability(el); - break; - } - } - toggleInputValidity(el, true); -} - -/** Hookup listeners for yes/no togglers for form fields - * Parameters: - * - radioButtonName: The "name=" value for the radio buttons being used as togglers - * - elementIdToShowIfYes: The Id of the element (eg. a div) to show if selected value of the given - * radio button is true (hides this element if false) - * - elementIdToShowIfNo: The Id of the element (eg. a div) to show if selected value of the given - * radio button is false (hides this element if true) - * **/ -function HookupYesNoListener(radioButtonName, elementIdToShowIfYes, elementIdToShowIfNo) { - HookupRadioTogglerListener(radioButtonName, { - 'True': elementIdToShowIfYes, - 'False': elementIdToShowIfNo - }); -} - -/** - * Hookup listeners for radio togglers in form fields. - * - * Parameters: - * - radioButtonName: The "name=" value for the radio buttons being used as togglers - * - valueToElementMap: An object where keys are the values of the radio buttons, - * and values are the corresponding DOM element IDs to show. All other elements will be hidden. - * - * Usage Example: - * Assuming you have radio buttons with values 'option1', 'option2', and 'option3', - * and corresponding DOM IDs 'section1', 'section2', 'section3'. - * - * HookupValueBasedListener('exampleRadioGroup', { - * 'option1': 'section1', - * 'option2': 'section2', - * 'option3': 'section3' - * }); - **/ -function HookupRadioTogglerListener(radioButtonName, valueToElementMap) { - // Get the radio buttons - let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]'); - - // Extract the list of all element IDs from the valueToElementMap - let allElementIds = Object.values(valueToElementMap); - - function handleRadioButtonChange() { - // Find the checked radio button - let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked'); - let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; - - // Hide all elements by default - allElementIds.forEach(function (elementId) { - let element = document.getElementById(elementId); - if (element) { - hideElement(element); - } - }); - - // Show the relevant element for the selected value - if (selectedValue && valueToElementMap[selectedValue]) { - let elementToShow = document.getElementById(valueToElementMap[selectedValue]); - if (elementToShow) { - showElement(elementToShow); - } - } - } - - if (radioButtons.length) { - // Add event listener to each radio button - radioButtons.forEach(function (radioButton) { - radioButton.addEventListener('change', handleRadioButtonChange); - }); - - // Initialize by checking the current state - handleRadioButtonChange(); - } -} - - -// A generic display none/block toggle function that takes an integer param to indicate how the elements toggle -function toggleTwoDomElements(ele1, ele2, index) { - let element1 = document.getElementById(ele1); - let element2 = document.getElementById(ele2); - if (element1 || element2) { - // Toggle display based on the index - if (element1) {element1.style.display = index === 1 ? 'block' : 'none';} - if (element2) {element2.style.display = index === 2 ? 'block' : 'none';} - } - else { - console.error('Unable to find elements to toggle'); - } -} - -// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> -// Event handlers. - -/** On input change, handles running any associated validators. */ -function handleInputValidation(e) { - clearValidators(e.target); - if (e.target.hasAttribute("auto-validate")) runValidators(e.target); -} - -/** On button click, handles running any associated validators. */ -function validateFieldInput(e) { - const attribute = e.target.getAttribute("validate-for") || ""; - if (!attribute.length) return; - const input = document.getElementById(attribute); - removeFormErrors(input, true); - runValidators(input); -} - - -function validateFormsetInputs(e, availabilityButton) { - - // Collect input IDs from the repeatable forms - let inputs = Array.from(document.querySelectorAll('.repeatable-form input')) - - // Run validators for each input - inputs.forEach(input => { - removeFormErrors(input, true); - runValidators(input); - }); - - // Set the validate-for attribute on the button with the collected input IDs - // Not needed for functionality but nice for accessibility - inputs = inputs.map(input => input.id).join(', '); - availabilityButton.setAttribute('validate-for', inputs); - -} - -// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> -// Initialization code. - -/** - * An IIFE that will attach validators to inputs. - * - * It looks for elements with `validate=" "` and adds change handlers. - * - * These handlers know about two other attributes: - * - `validate-for=""` creates a button which will run the validator(s) on - * - `auto-validate` will run validator(s) when the user stops typing (otherwise, - * they will only run when a user clicks the button with `validate-for`) - */ - (function validatorsInit() { - "use strict"; - const needsValidation = document.querySelectorAll('[validate]'); - for(const input of needsValidation) { - input.addEventListener('input', handleInputValidation); - } - const alternativeDomainsAvailability = document.getElementById('validate-alt-domains-availability'); - const activatesValidation = document.querySelectorAll('[validate-for]'); - - for(const button of activatesValidation) { - // Adds multi-field validation for alternative domains - if (button === alternativeDomainsAvailability) { - button.addEventListener('click', (e) => { - validateFormsetInputs(e, alternativeDomainsAvailability) - }); - } else { - button.addEventListener('click', validateFieldInput); - } - } -})(); - -/** - * Removes form errors surrounding a form input - */ -function removeFormErrors(input, removeStaleAlerts=false){ - // Remove error message - let errorMessage = document.getElementById(`${input.id}__error-message`); - if (errorMessage) { - errorMessage.remove(); - }else{ - return - } - - // Remove error classes - if (input.classList.contains('usa-input--error')) { - input.classList.remove('usa-input--error'); - } - - // Get the form label - let label = document.querySelector(`label[for="${input.id}"]`); - if (label) { - label.classList.remove('usa-label--error'); - - // Remove error classes from parent div - let parentDiv = label.parentElement; - if (parentDiv) { - parentDiv.classList.remove('usa-form-group--error'); - } - } - - if (removeStaleAlerts){ - let staleAlerts = document.querySelectorAll(".usa-alert--error") - for (let alert of staleAlerts){ - // Don't remove the error associated with the input - if (alert.id !== `${input.id}--toast`) { - alert.remove() - } - } - } -} - -/** - * Prepare the namerservers and DS data forms delete buttons - * We will call this on the forms init, and also every time we add a form - * - */ -function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){ - let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`); - let formToRemove = e.target.closest(".repeatable-form"); - formToRemove.remove(); - let forms = document.querySelectorAll(".repeatable-form"); - totalForms.setAttribute('value', `${forms.length}`); - - let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g'); - let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g'); - // For the example on Nameservers - let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g'); - - forms.forEach((form, index) => { - // Iterate over child nodes of the current element - Array.from(form.querySelectorAll('label, input, select')).forEach((node) => { - // Iterate through the attributes of the current node - Array.from(node.attributes).forEach((attr) => { - // Check if the attribute value matches the regex - if (formNumberRegex.test(attr.value)) { - // Replace the attribute value with the updated value - attr.value = attr.value.replace(formNumberRegex, `form-${index}-`); - } - }); - }); - - // h2 and legend for DS form, label for nameservers - Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => { - - let innerSpan = node.querySelector('span') - if (innerSpan) { - innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`); - } else { - node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`); - node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`); - } - - // If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required) - // inject the USWDS required markup and make sure the INPUT is required - if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) { - - // Remove the word optional - innerSpan.textContent = innerSpan.textContent.replace(/\s*\(\s*optional\s*\)\s*/, ''); - - // Create a new element - const newElement = document.createElement('abbr'); - newElement.textContent = '*'; - newElement.setAttribute("title", "required"); - newElement.classList.add("usa-hint", "usa-hint--required"); - - // Append the new element to the label - node.appendChild(newElement); - // Find the next sibling that is an input element - let nextInputElement = node.nextElementSibling; - - while (nextInputElement) { - if (nextInputElement.tagName === 'INPUT') { - // Found the next input element - nextInputElement.setAttribute("required", "") - break; - } - nextInputElement = nextInputElement.nextElementSibling; - } - nextInputElement.required = true; - } - - - - }); - - // Display the add more button if we have less than 13 forms - if (isNameserversForm && forms.length <= 13) { - addButton.removeAttribute("disabled"); - } - - if (isNameserversForm && forms.length < 3) { - // Hide the delete buttons on the remaining nameservers - Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => { - deleteButton.setAttribute("disabled", "true"); - }); - } - - }); -} - -/** - * Delete method for formsets using the DJANGO DELETE widget (Other Contacts) - * - */ -function markForm(e, formLabel){ - // Unlike removeForm, we only work with the visible forms when using DJANGO's DELETE widget - let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length; - - if (totalShownForms == 1) { - // toggle the radio buttons - let radioButton = document.querySelector('input[name="other_contacts-has_other_contacts"][value="False"]'); - radioButton.checked = true; - // Trigger the change event - let event = new Event('change'); - radioButton.dispatchEvent(event); - } else { - - // Grab the hidden delete input and assign a value DJANGO will look for - let formToRemove = e.target.closest(".repeatable-form"); - if (formToRemove) { - let deleteInput = formToRemove.querySelector('input[class="deletion"]'); - if (deleteInput) { - deleteInput.value = 'on'; - } - } - - // Set display to 'none' - formToRemove.style.display = 'none'; - } - - // Update h2s on the visible forms only. We won't worry about the forms' identifiers - let shownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`); - let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g'); - shownForms.forEach((form, index) => { - // Iterate over child nodes of the current element - Array.from(form.querySelectorAll('h2')).forEach((node) => { - node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`); - }); - }); -} - -/** - * Prepare the namerservers, DS data and Other Contacts formsets' delete button - * for the last added form. We call this from the Add function - * - */ -function prepareNewDeleteButton(btn, formLabel) { - let formIdentifier = "form" - let isNameserversForm = document.querySelector(".nameservers-form"); - let isOtherContactsForm = document.querySelector(".other-contacts-form"); - let addButton = document.querySelector("#add-form"); - - if (isOtherContactsForm) { - formIdentifier = "other_contacts"; - // We will mark the forms for deletion - btn.addEventListener('click', function(e) { - markForm(e, formLabel); - }); - } else { - // We will remove the forms and re-order the formset - btn.addEventListener('click', function(e) { - removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier); - }); - } -} - -/** - * Prepare the namerservers, DS data and Other Contacts formsets' delete buttons - * We will call this on the forms init - * - */ -function prepareDeleteButtons(formLabel) { - let formIdentifier = "form" - let deleteButtons = document.querySelectorAll(".delete-record"); - let isNameserversForm = document.querySelector(".nameservers-form"); - let isOtherContactsForm = document.querySelector(".other-contacts-form"); - let addButton = document.querySelector("#add-form"); - if (isOtherContactsForm) { - formIdentifier = "other_contacts"; - } - - // Loop through each delete button and attach the click event listener - deleteButtons.forEach((deleteButton) => { - if (isOtherContactsForm) { - // We will mark the forms for deletion - deleteButton.addEventListener('click', function(e) { - markForm(e, formLabel); - }); - } else { - // We will remove the forms and re-order the formset - deleteButton.addEventListener('click', function(e) { - removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier); - }); - } - }); -} - -/** - * DJANGO formset's DELETE widget - * On form load, hide deleted forms, ie. those forms with hidden input of class 'deletion' - * with value='on' - */ -function hideDeletedForms() { - let hiddenDeleteButtonsWithValueOn = document.querySelectorAll('input[type="hidden"].deletion[value="on"]'); - - // Iterating over the NodeList of hidden inputs - hiddenDeleteButtonsWithValueOn.forEach(function(hiddenInput) { - // Finding the closest parent element with class "repeatable-form" for each hidden input - var repeatableFormToHide = hiddenInput.closest('.repeatable-form'); - - // Checking if a matching parent element is found for each hidden input - if (repeatableFormToHide) { - // Setting the display property to "none" for each matching parent element - repeatableFormToHide.style.display = 'none'; - } - }); -} - -// Checks for if we want to display Urbanization or not -document.addEventListener('DOMContentLoaded', function() { - var stateTerritoryField = document.querySelector('select[name="organization_contact-state_territory"]'); - - if (!stateTerritoryField) { - return; // Exit if the field not found - } - - setupUrbanizationToggle(stateTerritoryField); -}); - -function setupUrbanizationToggle(stateTerritoryField) { - var urbanizationField = document.getElementById('urbanization-field'); - - function toggleUrbanizationField() { - // Checking specifically for Puerto Rico only - if (stateTerritoryField.value === 'PR') { - urbanizationField.style.display = 'block'; - } else { - urbanizationField.style.display = 'none'; - } - } - - toggleUrbanizationField(); - - stateTerritoryField.addEventListener('change', toggleUrbanizationField); -} - -/** - * An IIFE that attaches a click handler for our dynamic formsets - * - * Only does something on a few pages, but it should be fast enough to run - * it everywhere. - */ -(function prepareFormsetsForms() { - let formIdentifier = "form" - let repeatableForm = document.querySelectorAll(".repeatable-form"); - let container = document.querySelector("#form-container"); - let addButton = document.querySelector("#add-form"); - let cloneIndex = 0; - let formLabel = ''; - let isNameserversForm = document.querySelector(".nameservers-form"); - let isOtherContactsForm = document.querySelector(".other-contacts-form"); - let isDsDataForm = document.querySelector(".ds-data-form"); - let isDotgovDomain = document.querySelector(".dotgov-domain-form"); - // The Nameservers formset features 2 required and 11 optionals - if (isNameserversForm) { - // cloneIndex = 2; - formLabel = "Name server"; - // DNSSEC: DS Data - } else if (isDsDataForm) { - formLabel = "DS data record"; - // The Other Contacts form - } else if (isOtherContactsForm) { - formLabel = "Organization contact"; - container = document.querySelector("#other-employees"); - formIdentifier = "other_contacts" - } else if (isDotgovDomain) { - formIdentifier = "dotgov_domain" - } - let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`); - - // On load: Disable the add more button if we have 13 forms - if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) { - addButton.setAttribute("disabled", "true"); - } - - // Hide forms which have previously been deleted - hideDeletedForms() - - // Attach click event listener on the delete buttons of the existing forms - prepareDeleteButtons(formLabel); - - if (addButton) - addButton.addEventListener('click', addForm); - - function addForm(e){ - let forms = document.querySelectorAll(".repeatable-form"); - let formNum = forms.length; - let newForm = repeatableForm[cloneIndex].cloneNode(true); - let formNumberRegex = RegExp(`${formIdentifier}-(\\d){1}-`,'g'); - let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g'); - // For the eample on Nameservers - let formExampleRegex = RegExp(`ns(\\d){1}`, 'g'); - - // Some Nameserver form checks since the delete can mess up the source object we're copying - // in regards to required fields and hidden delete buttons - if (isNameserversForm) { - - // If the source element we're copying has required on an input, - // reset that input - let formRequiredNeedsCleanUp = newForm.innerHTML.includes('*'); - if (formRequiredNeedsCleanUp) { - newForm.querySelector('label abbr').remove(); - // Get all input elements within the container - const inputElements = newForm.querySelectorAll("input"); - // Loop through each input element and remove the 'required' attribute - inputElements.forEach((input) => { - if (input.hasAttribute("required")) { - input.removeAttribute("required"); - } - }); - } - - // If the source element we're copying has an disabled delete button, - // enable that button - let deleteButton= newForm.querySelector('.delete-record'); - if (deleteButton.hasAttribute("disabled")) { - deleteButton.removeAttribute("disabled"); - } - } - - formNum++; - - newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`); - if (isOtherContactsForm) { - // For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden, - // since the form on the backend employs Django's DELETE widget. - let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length; - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`); - } else { - // Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional - // if indices 0 or 1 have been deleted - let containsOptional = newForm.innerHTML.includes('(optional)'); - if (isNameserversForm && !containsOptional) { - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum} (optional)`); - } else { - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`); - } - } - newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`); - newForm.innerHTML = newForm.innerHTML.replace(/\n/g, ''); // Remove newline characters - newForm.innerHTML = newForm.innerHTML.replace(/>\s*<'); // Remove spaces between tags - container.insertBefore(newForm, addButton); - - newForm.style.display = 'block'; - - let inputs = newForm.querySelectorAll("input"); - // Reset the values of each input to blank - inputs.forEach((input) => { - input.classList.remove("usa-input--error"); - input.classList.remove("usa-input--success"); - if (input.type === "text" || input.type === "number" || input.type === "password" || input.type === "email" || input.type === "tel") { - input.value = ""; // Set the value to an empty string - - } else if (input.type === "checkbox" || input.type === "radio") { - input.checked = false; // Uncheck checkboxes and radios - } - }); - - // Reset any existing validation classes - let selects = newForm.querySelectorAll("select"); - selects.forEach((select) => { - select.classList.remove("usa-input--error"); - select.classList.remove("usa-input--success"); - select.selectedIndex = 0; // Set the value to an empty string - }); - - let labels = newForm.querySelectorAll("label"); - labels.forEach((label) => { - label.classList.remove("usa-label--error"); - label.classList.remove("usa-label--success"); - }); - - let usaFormGroups = newForm.querySelectorAll(".usa-form-group"); - usaFormGroups.forEach((usaFormGroup) => { - usaFormGroup.classList.remove("usa-form-group--error"); - usaFormGroup.classList.remove("usa-form-group--success"); - }); - - // Remove any existing error and success messages - let usaMessages = newForm.querySelectorAll(".usa-error-message, .usa-alert"); - usaMessages.forEach((usaErrorMessage) => { - let parentDiv = usaErrorMessage.closest('div'); - if (parentDiv) { - parentDiv.remove(); // Remove the parent div if it exists - } - }); - - totalForms.setAttribute('value', `${formNum}`); - - // Attach click event listener on the delete buttons of the new form - let newDeleteButton = newForm.querySelector(".delete-record"); - if (newDeleteButton) - prepareNewDeleteButton(newDeleteButton, formLabel); - - // Disable the add more button if we have 13 forms - if (isNameserversForm && formNum == 13) { - addButton.setAttribute("disabled", "true"); - } - - if (isNameserversForm && forms.length >= 2) { - // Enable the delete buttons on the nameservers - forms.forEach((form, index) => { - Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => { - deleteButton.removeAttribute("disabled"); - }); - }); - } - } -})(); - -/** - * An IIFE that triggers a modal on the DS Data Form under certain conditions - * - */ -(function triggerModalOnDsDataForm() { - let saveButon = document.querySelector("#save-ds-data"); - - // The view context will cause a hitherto hidden modal trigger to - // show up. On save, we'll test for that modal trigger appearing. We'll - // run that test once every 100 ms for 5 secs, which should balance performance - // while accounting for network or lag issues. - if (saveButon) { - let i = 0; - var tryToTriggerModal = setInterval(function() { - i++; - if (i > 100) { - clearInterval(tryToTriggerModal); - } - let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert"); - if (modalTrigger) { - modalTrigger.click() - clearInterval(tryToTriggerModal); - } - }, 50); - } -})(); - - -/** - * An IIFE that listens to the other contacts radio form on DAs and toggles the contacts/no other contacts forms - * - */ -(function otherContactsFormListener() { - HookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees') -})(); - - -/** - * An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly - * - */ -(function anythingElseFormListener() { - HookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null) -})(); - - -/** - * An IIFE that listens to the yes/no radio buttons on the anything else form and toggles form field visibility accordingly - * - */ -(function newMemberFormListener() { - HookupRadioTogglerListener('member_access_level', { - 'admin': 'new-member-admin-permissions', - 'basic': 'new-member-basic-permissions' - }); -})(); - -/** - * An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms - * - */ -(function nameserversFormListener() { - let isNameserversForm = document.querySelector(".nameservers-form"); - if (isNameserversForm) { - let forms = document.querySelectorAll(".repeatable-form"); - if (forms.length < 3) { - // Hide the delete buttons on the 2 nameservers - forms.forEach((form) => { - Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => { - deleteButton.setAttribute("disabled", "true"); - }); - }); - } - } -})(); - -/** - * An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms - * - */ -(function nameserversFormListener() { - let isNameserversForm = document.querySelector(".nameservers-form"); - if (isNameserversForm) { - let forms = document.querySelectorAll(".repeatable-form"); - if (forms.length < 3) { - // Hide the delete buttons on the 2 nameservers - forms.forEach((form) => { - Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => { - deleteButton.setAttribute("disabled", "true"); - }); - }); - } - } -})(); - -/** - * An IIFE that listens to the yes/no radio buttons on the CISA representatives form and toggles form field visibility accordingly - * - */ -(function cisaRepresentativesFormListener() { - HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null) -})(); - -/** - * Initialize USWDS tooltips by calling initialization method. Requires that uswds-edited.js - * be loaded before get-gov.js. uswds-edited.js adds the tooltip module to the window to be - * accessible directly in get-gov.js - * - */ -function initializeTooltips() { - function checkTooltip() { - // Check that the tooltip library is loaded, and if not, wait and retry - if (window.tooltip && typeof window.tooltip.init === 'function') { - window.tooltip.init(); - } else { - // Retry after a short delay - setTimeout(checkTooltip, 100); - } - } - checkTooltip(); -} - -/** - * Initialize USWDS modals by calling on method. Requires that uswds-edited.js be loaded - * before get-gov.js. uswds-edited.js adds the modal module to the window to be accessible - * directly in get-gov.js. - * initializeModals adds modal-related DOM elements, based on other DOM elements existing in - * the page. It needs to be called only once for any particular DOM element; otherwise, it - * will initialize improperly. Therefore, if DOM elements change dynamically and include - * DOM elements with modal classes, unloadModals needs to be called before initializeModals. - * - */ -function initializeModals() { - window.modal.on(); -} - -/** - * Unload existing USWDS modals by calling off method. Requires that uswds-edited.js be - * loaded before get-gov.js. uswds-edited.js adds the modal module to the window to be - * accessible directly in get-gov.js. - * See note above with regards to calling this method relative to initializeModals. - * - */ -function unloadModals() { - window.modal.off(); -} - -class LoadTableBase { - constructor(sectionSelector) { - this.tableWrapper = document.getElementById(`${sectionSelector}__table-wrapper`); - this.tableHeaders = document.querySelectorAll(`#${sectionSelector} th[data-sortable]`); - this.currentSortBy = 'id'; - this.currentOrder = 'asc'; - this.currentStatus = []; - this.currentSearchTerm = ''; - this.scrollToTable = false; - this.searchInput = document.getElementById(`${sectionSelector}__search-field`); - this.searchSubmit = document.getElementById(`${sectionSelector}__search-field-submit`); - this.tableAnnouncementRegion = document.getElementById(`${sectionSelector}__usa-table__announcement-region`); - this.resetSearchButton = document.getElementById(`${sectionSelector}__reset-search`); - this.resetFiltersButton = document.getElementById(`${sectionSelector}__reset-filters`); - this.statusCheckboxes = document.querySelectorAll(`.${sectionSelector} input[name="filter-status"]`); - this.statusIndicator = document.getElementById(`${sectionSelector}__filter-indicator`); - this.statusToggle = document.getElementById(`${sectionSelector}__usa-button--filter`); - this.noTableWrapper = document.getElementById(`${sectionSelector}__no-data`); - this.noSearchResultsWrapper = document.getElementById(`${sectionSelector}__no-search-results`); - this.portfolioElement = document.getElementById('portfolio-js-value'); - this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; - this.initializeTableHeaders(); - this.initializeSearchHandler(); - this.initializeStatusToggleHandler(); - this.initializeFilterCheckboxes(); - this.initializeResetSearchButton(); - this.initializeResetFiltersButton(); - this.initializeAccordionAccessibilityListeners(); - } - - /** - * Generalized function to update pagination for a list. - * @param {string} itemName - The name displayed in the counter - * @param {string} paginationSelector - CSS selector for the pagination container. - * @param {string} counterSelector - CSS selector for the pagination counter. - * @param {string} tableSelector - CSS selector for the header element to anchor the links to. - * @param {number} currentPage - The current page number (starting with 1). - * @param {number} numPages - The total number of pages. - * @param {boolean} hasPrevious - Whether there is a page before the current page. - * @param {boolean} hasNext - Whether there is a page after the current page. - * @param {number} total - The total number of items. - */ - updatePagination( - itemName, - paginationSelector, - counterSelector, - parentTableSelector, - currentPage, - numPages, - hasPrevious, - hasNext, - totalItems, - ) { - const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); - const counterSelectorEl = document.querySelector(counterSelector); - const paginationSelectorEl = document.querySelector(paginationSelector); - counterSelectorEl.innerHTML = ''; - paginationButtons.innerHTML = ''; - - // Buttons should only be displayed if there are more than one pages of results - paginationButtons.classList.toggle('display-none', numPages <= 1); - - // Counter should only be displayed if there is more than 1 item - paginationSelectorEl.classList.toggle('display-none', totalItems < 1); - - counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`; - - if (hasPrevious) { - const prevPageItem = document.createElement('li'); - prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - prevPageItem.innerHTML = ` - - - Previous - - `; - prevPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - this.loadTable(currentPage - 1); - }); - paginationButtons.appendChild(prevPageItem); - } - - // Add first page and ellipsis if necessary - if (currentPage > 2) { - paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage)); - if (currentPage > 3) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } - } - - // Add pages around the current page - for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { - paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage)); - } - - // Add last page and ellipsis if necessary - if (currentPage < numPages - 1) { - if (currentPage < numPages - 2) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } - paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage)); - } - - if (hasNext) { - const nextPageItem = document.createElement('li'); - nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - nextPageItem.innerHTML = ` - - Next - - - `; - nextPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - this.loadTable(currentPage + 1); - }); - paginationButtons.appendChild(nextPageItem); - } - } - - /** - * A helper that toggles content/ no content/ no search results - * - */ - updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { - const { unfiltered_total, total } = data; - if (unfiltered_total) { - if (total) { - showElement(dataWrapper); - hideElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } else { - hideElement(dataWrapper); - showElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } - } else { - hideElement(dataWrapper); - hideElement(noSearchResultsWrapper); - showElement(noDataWrapper); - } - }; - - // Helper function to create a page item - createPageItem(page, parentTableSelector, currentPage) { - const pageItem = document.createElement('li'); - pageItem.className = 'usa-pagination__item usa-pagination__page-no'; - pageItem.innerHTML = ` - ${page} - `; - if (page === currentPage) { - pageItem.querySelector('a').classList.add('usa-current'); - pageItem.querySelector('a').setAttribute('aria-current', 'page'); - } - pageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - this.loadTable(page); - }); - return pageItem; - } - - /** - * A helper that resets sortable table headers - * - */ - unsetHeader = (header) => { - header.removeAttribute('aria-sort'); - let headerName = header.innerText; - const headerLabel = `${headerName}, sortable column, currently unsorted"`; - const headerButtonLabel = `Click to sort by ascending order.`; - header.setAttribute("aria-label", headerLabel); - header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); - }; - - // Abstract method (to be implemented in the child class) - loadTable(page, sortBy, order) { - throw new Error('loadData() must be implemented in a subclass'); - } - - // Add event listeners to table headers for sorting - initializeTableHeaders() { - this.tableHeaders.forEach(header => { - header.addEventListener('click', () => { - const sortBy = header.getAttribute('data-sortable'); - let order = 'asc'; - // sort order will be ascending, unless the currently sorted column is ascending, and the user - // is selecting the same column to sort in descending order - if (sortBy === this.currentSortBy) { - order = this.currentOrder === 'asc' ? 'desc' : 'asc'; - } - // load the results with the updated sort - this.loadTable(1, sortBy, order); - }); - }); - } - - initializeSearchHandler() { - this.searchSubmit.addEventListener('click', (e) => { - e.preventDefault(); - this.currentSearchTerm = this.searchInput.value; - // If the search is blank, we match the resetSearch functionality - if (this.currentSearchTerm) { - showElement(this.resetSearchButton); - } else { - hideElement(this.resetSearchButton); - } - this.loadTable(1, 'id', 'asc'); - this.resetHeaders(); - }); - } - - initializeStatusToggleHandler() { - if (this.statusToggle) { - this.statusToggle.addEventListener('click', () => { - toggleCaret(this.statusToggle); - }); - } - } - - // Add event listeners to status filter checkboxes - initializeFilterCheckboxes() { - this.statusCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', () => { - const checkboxValue = checkbox.value; - - // Update currentStatus array based on checkbox state - if (checkbox.checked) { - this.currentStatus.push(checkboxValue); - } else { - const index = this.currentStatus.indexOf(checkboxValue); - if (index > -1) { - this.currentStatus.splice(index, 1); - } - } - - // Manage visibility of reset filters button - if (this.currentStatus.length == 0) { - hideElement(this.resetFiltersButton); - } else { - showElement(this.resetFiltersButton); - } - - // Disable the auto scroll - this.scrollToTable = false; - - // Call loadTable with updated status - this.loadTable(1, 'id', 'asc'); - this.resetHeaders(); - this.updateStatusIndicator(); - }); - }); - } - - // Reset UI and accessibility - resetHeaders() { - this.tableHeaders.forEach(header => { - // Unset sort UI in headers - this.unsetHeader(header); - }); - // Reset the announcement region - this.tableAnnouncementRegion.innerHTML = ''; - } - - resetSearch() { - this.searchInput.value = ''; - this.currentSearchTerm = ''; - hideElement(this.resetSearchButton); - this.loadTable(1, 'id', 'asc'); - this.resetHeaders(); - } - - initializeResetSearchButton() { - if (this.resetSearchButton) { - this.resetSearchButton.addEventListener('click', () => { - this.resetSearch(); - }); - } - } - - resetFilters() { - this.currentStatus = []; - this.statusCheckboxes.forEach(checkbox => { - checkbox.checked = false; - }); - hideElement(this.resetFiltersButton); - - // Disable the auto scroll - this.scrollToTable = false; - - this.loadTable(1, 'id', 'asc'); - this.resetHeaders(); - this.updateStatusIndicator(); - // No need to toggle close the filters. The focus shift will trigger that for us. - } - - initializeResetFiltersButton() { - if (this.resetFiltersButton) { - this.resetFiltersButton.addEventListener('click', () => { - this.resetFilters(); - }); - } - } - - updateStatusIndicator() { - this.statusIndicator.innerHTML = ''; - // Even if the element is empty, it'll mess up the flex layout unless we set display none - hideElement(this.statusIndicator); - if (this.currentStatus.length) - this.statusIndicator.innerHTML = '(' + this.currentStatus.length + ')'; - showElement(this.statusIndicator); - } - - closeFilters() { - if (this.statusToggle.getAttribute("aria-expanded") === "true") { - this.statusToggle.click(); - } - } - - initializeAccordionAccessibilityListeners() { - // Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter, - // user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button. - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('focusin', (event) => { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - - if (accordionThatIsOpen && !accordion.contains(event.target)) { - this.closeFilters(); - } - }); - - // Close when user clicks outside - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('click', (event) => { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - - if (accordionThatIsOpen && !accordion.contains(event.target)) { - this.closeFilters(); - } - }); - } -} - -class DomainsTable extends LoadTableBase { - - constructor() { - super('domains'); - } - /** - * Loads rows in the domains list, as well as updates pagination around the domains list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} status - control for the status filter - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - - // fetch json of page of domais, given params - let baseUrl = document.getElementById("get_domains_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - // fetch json of page of domains, given params - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "status": status, - "search_term": searchTerm - } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) - - let url = `${baseUrlValue}?${searchParams.toString()}` - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no domains exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const domainList = document.querySelector('#domains tbody'); - domainList.innerHTML = ''; - - data.domains.forEach(domain => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null; - const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : ''; - const expirationDateSortValue = expirationDate ? expirationDate.getTime() : ''; - const actionUrl = domain.action_url; - const suborganization = domain.domain_info__sub_organization ? domain.domain_info__sub_organization : '⎯'; - - const row = document.createElement('tr'); - - let markupForSuborganizationRow = ''; - - if (this.portfolioValue) { - markupForSuborganizationRow = ` - - ${suborganization} - - ` - } - - row.innerHTML = ` - - ${domain.name} - - - ${expirationDateFormatted} - - - ${domain.state_display} - - - - - ${markupForSuborganizationRow} - - - - ${domain.action_label} ${domain.name} - - - `; - domainList.appendChild(row); - }); - // initialize tool tips immediately after the associated DOM elements are added - initializeTooltips(); - - // Do not scroll on first page load - if (scroll) - ScrollToElement('class', 'domains'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'domain', - '#domains-pagination', - '#domains-pagination .usa-pagination__counter', - '#domains', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domains:', error)); - } -} - -class DomainRequestsTable extends LoadTableBase { - - constructor() { - super('domain-requests'); - } - - toggleExportButton(requests) { - const exportButton = document.getElementById('export-csv'); - if (exportButton) { - if (requests.length > 0) { - showElement(exportButton); - } else { - hideElement(exportButton); - } - } -} - - /** - * Loads rows in the domains list, as well as updates pagination around the domains list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} status - control for the status filter - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) { - let baseUrl = document.getElementById("get_domain_requests_json_url"); - - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - // add searchParams - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "status": status, - "search_term": searchTerm - } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) - - let url = `${baseUrlValue}?${searchParams.toString()}` - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // Manage "export as CSV" visibility for domain requests - this.toggleExportButton(data.domain_requests); - - // handle the display of proper messaging in the event that no requests exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain request list will be inserted into the DOM - const tbody = document.querySelector('#domain-requests tbody'); - tbody.innerHTML = ''; - - // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases - // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, - // which will cause bad delete requests to be sent. - const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); - preExistingModalPlaceholders.forEach(element => { - element.remove(); - }); - - // remove any existing modal elements from the DOM so they can be properly re-initialized - // after the DOM content changes and there are new delete modal buttons added - unloadModals(); - - let needsDeleteColumn = false; - - needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); - - // Remove existing delete th and td if they exist - let existingDeleteTh = document.querySelector('.delete-header'); - if (!needsDeleteColumn) { - if (existingDeleteTh) - existingDeleteTh.remove(); - } else { - if (!existingDeleteTh) { - const delheader = document.createElement('th'); - delheader.setAttribute('scope', 'col'); - delheader.setAttribute('role', 'columnheader'); - delheader.setAttribute('class', 'delete-header'); - delheader.innerHTML = ` - Delete Action`; - let tableHeaderRow = document.querySelector('#domain-requests thead tr'); - tableHeaderRow.appendChild(delheader); - } - } - - data.domain_requests.forEach(request => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const domainName = request.requested_domain ? request.requested_domain : `New domain request
      (${utcDateString(request.created_at)})`; - const actionUrl = request.action_url; - const actionLabel = request.action_label; - const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; - - // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) - // If the request is not deletable, use the following (hidden) span for ANDI screenreaders to indicate this state to the end user - let modalTrigger = ` - Domain request cannot be deleted now. Edit the request for more information.`; - - let markupCreatorRow = ''; - - if (this.portfolioValue) { - markupCreatorRow = ` - - ${request.creator ? request.creator : ''} - - ` - } - - if (request.is_deletable) { - // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages - let modalHeading = ''; - let modalDescription = ''; - - if (request.requested_domain) { - modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } else { - if (request.created_at) { - modalHeading = 'Are you sure you want to delete this domain request?'; - modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; - } else { - modalHeading = 'Are you sure you want to delete New domain request?'; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } - } - - modalTrigger = ` - - Delete ${domainName} - ` - - const modalSubmit = ` - - ` - - const modal = document.createElement('div'); - modal.setAttribute('class', 'usa-modal'); - modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); - modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); - modal.setAttribute('aria-describedby', 'Domain will be removed'); - modal.setAttribute('data-force-action', ''); - - modal.innerHTML = ` -
      -
      - -
      - -
      - -
      - -
      - ` - - this.tableWrapper.appendChild(modal); - - // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly - if (this.portfolioValue) { - modalTrigger = ` - - Delete ${domainName} - - -
      -
      - -
      - -
      - ` - } - } - - - const row = document.createElement('tr'); - row.innerHTML = ` - - ${domainName} - - - ${submissionDate} - - ${markupCreatorRow} - - ${request.status} - - - - - ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} - - - ${needsDeleteColumn ? ''+modalTrigger+'' : ''} - `; - tbody.appendChild(row); - }); - - // initialize modals immediately after the DOM content is updated - initializeModals(); - - // Now the DOM and modals are ready, add listeners to the submit buttons - const modals = document.querySelectorAll('.usa-modal__content'); - - modals.forEach(modal => { - const submitButton = modal.querySelector('.usa-modal__submit'); - const closeButton = modal.querySelector('.usa-modal__close'); - submitButton.addEventListener('click', () => { - let pk = submitButton.getAttribute('data-pk'); - // Close the modal to remove the USWDS UI local classes - closeButton.click(); - // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page - let pageToDisplay = data.page; - if (data.total == 1 && data.unfiltered_total > 1) { - pageToDisplay--; - } - this.deleteDomainRequest(pk, pageToDisplay); - }); - }); - - // Do not scroll on first page load - if (scroll) - ScrollToElement('class', 'domain-requests'); - this.scrollToTable = true; - - // update the pagination after the domain requests list is updated - this.updatePagination( - 'domain request', - '#domain-requests-pagination', - '#domain-requests-pagination .usa-pagination__counter', - '#domain-requests', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } - - /** - * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. - * @param {*} domainRequestPk - the identifier for the request that we're deleting - * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page - */ - deleteDomainRequest(domainRequestPk, pageToDisplay) { - // Use to debug uswds modal issues - //console.log('deleteDomainRequest') - - // Get csrf token - const csrfToken = getCsrfToken(); - // Create FormData object and append the CSRF token - const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; - - fetch(`/domain-request/${domainRequestPk}/delete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - }, - body: formData - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Update data and UI - this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } -} - -class MembersTable extends LoadTableBase { - - constructor() { - super('members'); - } - - /** - * Initializes "Show More" buttons on the page, enabling toggle functionality to show or hide content. - * - * The function finds elements with "Show More" buttons and sets up a click event listener to toggle the visibility - * of a corresponding content div. When clicked, the button updates its visual state (e.g., text/icon change), - * and the associated content is shown or hidden based on its current visibility status. - * - * @function initShowMoreButtons - */ - initShowMoreButtons() { - /** - * Toggles the visibility of a content section when the "Show More" button is clicked. - * Updates the button text/icon based on whether the content is shown or hidden. - * - * @param {HTMLElement} toggleButton - The button that toggles the content visibility. - * @param {HTMLElement} contentDiv - The content div whose visibility is toggled. - * @param {HTMLElement} buttonParentRow - The parent row element containing the button. - */ - function toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow) { - const spanElement = toggleButton.querySelector('span'); - const useElement = toggleButton.querySelector('use'); - if (contentDiv.classList.contains('display-none')) { - showElement(contentDiv); - spanElement.textContent = 'Close'; - useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less'); - buttonParentRow.classList.add('hide-td-borders'); - toggleButton.setAttribute('aria-label', 'Close additional information'); - } else { - hideElement(contentDiv); - spanElement.textContent = 'Expand'; - useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more'); - buttonParentRow.classList.remove('hide-td-borders'); - toggleButton.setAttribute('aria-label', 'Expand for additional information'); - } - } - - let toggleButtons = document.querySelectorAll('.usa-button--show-more-button'); - toggleButtons.forEach((toggleButton) => { - - // get contentDiv for element specified in data-for attribute of toggleButton - let dataFor = toggleButton.dataset.for; - let contentDiv = document.getElementById(dataFor); - let buttonParentRow = toggleButton.parentElement.parentElement; - if (contentDiv && contentDiv.tagName.toLowerCase() === 'tr' && contentDiv.classList.contains('show-more-content') && buttonParentRow && buttonParentRow.tagName.toLowerCase() === 'tr') { - toggleButton.addEventListener('click', function() { - toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow); - }); - } else { - console.warn('Found a toggle button with no associated toggleable content or parent row'); - } - - }); - } - - /** - * Converts a given `last_active` value into a display value and a numeric sort value. - * The input can be a UTC date, the strings "Invited", "Invalid date", or null/undefined. - * - * @param {string} last_active - UTC date string or special status like "Invited" or "Invalid date". - * @returns {Object} - An object containing `display_value` (formatted date or status string) - * and `sort_value` (numeric value for sorting). - */ - handleLastActive(last_active) { - const invited = 'Invited'; - const invalid_date = 'Invalid date'; - const options = { year: 'numeric', month: 'long', day: 'numeric' }; // Date display format - - let display_value = invalid_date; // Default display value for invalid or null dates - let sort_value = -1; // Default sort value for invalid or null dates - - if (last_active === invited) { - // Handle "Invited" status: special case with 0 sort value - display_value = invited; - sort_value = 0; - } else if (last_active && last_active !== invalid_date) { - // Parse and format valid UTC date strings - const parsedDate = new Date(last_active); - - if (!isNaN(parsedDate.getTime())) { - // Valid date - display_value = parsedDate.toLocaleDateString('en-US', options); - sort_value = parsedDate.getTime(); // Use timestamp for sorting - } else { - console.error(`Error: Invalid date string provided: ${last_active}`); - } - } - - return { display_value, sort_value }; - } - - /** - * Generates HTML for the list of domains assigned to a member. - * - * @param {number} num_domains - The number of domains the member is assigned to. - * @param {Array} domain_names - An array of domain names. - * @param {Array} domain_urls - An array of corresponding domain URLs. - * @returns {string} - A string of HTML displaying the domains assigned to the member. - */ - generateDomainsHTML(num_domains, domain_names, domain_urls, action_url) { - // Initialize an empty string for the HTML - let domainsHTML = ''; - - // Only generate HTML if the member has one or more assigned domains - if (num_domains > 0) { - domainsHTML += "
      "; - domainsHTML += "

      Domains assigned

      "; - domainsHTML += `

      This member is assigned to ${num_domains} domains:

      `; - domainsHTML += "
        "; - - // Display up to 6 domains with their URLs - for (let i = 0; i < num_domains && i < 6; i++) { - domainsHTML += `
      • ${domain_names[i]}
      • `; - } - - domainsHTML += "
      "; - - // If there are more than 6 domains, display a "View assigned domains" link - if (num_domains >= 6) { - domainsHTML += `

      View assigned domains

      `; - } - - domainsHTML += "
      "; - } - - return domainsHTML; - } - - /** - * Generates an HTML string summarizing a user's additional permissions within a portfolio, - * based on the user's permissions and predefined permission choices. - * - * @param {Array} member_permissions - An array of permission strings that the member has. - * @param {Object} UserPortfolioPermissionChoices - An object containing predefined permission choice constants. - * Expected keys include: - * - VIEW_ALL_DOMAINS - * - VIEW_MANAGED_DOMAINS - * - EDIT_REQUESTS - * - VIEW_ALL_REQUESTS - * - EDIT_MEMBERS - * - VIEW_MEMBERS - * - * @returns {string} - A string of HTML representing the user's additional permissions. - * If the user has no specific permissions, it returns a default message - * indicating no additional permissions. - * - * Behavior: - * - The function checks the user's permissions (`member_permissions`) and generates - * corresponding HTML sections based on the permission choices defined in `UserPortfolioPermissionChoices`. - * - Permissions are categorized into domains, requests, and members: - * - Domains: Determines whether the user can view or manage all or assigned domains. - * - Requests: Differentiates between users who can edit requests, view all requests, or have no request privileges. - * - Members: Distinguishes between members who can manage or only view other members. - * - If no relevant permissions are found, the function returns a message stating that the user has no additional permissions. - * - The resulting HTML always includes a header "Additional permissions for this member" and appends the relevant permission descriptions. - */ - generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) { - let permissionsHTML = ''; - - // Check domain-related permissions - if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) { - permissionsHTML += "

      Domains: Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).

      "; - } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) { - permissionsHTML += "

      Domains: Can manage domains they are assigned to and edit information about the domain (including DNS settings).

      "; - } - - // Check request-related permissions - if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) { - permissionsHTML += "

      Domain requests: Can view all organization domain requests. Can create domain requests and modify their own requests.

      "; - } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) { - permissionsHTML += "

      Domain requests (view-only): Can view all organization domain requests. Can't create or modify any domain requests.

      "; - } - - // Check member-related permissions - if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) { - permissionsHTML += "

      Members: Can manage members including inviting new members, removing current members, and assigning domains to members.

      "; - } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) { - permissionsHTML += "

      Members (view-only): Can view all organizational members. Can't manage any members.

      "; - } - - // If no specific permissions are assigned, display a message indicating no additional permissions - if (!permissionsHTML) { - permissionsHTML += "

      No additional permissions: There are no additional permissions for this member.

      "; - } - - // Add a permissions header and wrap the entire output in a container - permissionsHTML = "

      Additional permissions for this member

      " + permissionsHTML + "
      "; - - return permissionsHTML; - } - - /** - * Loads rows in the members list, as well as updates pagination around the members list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - - // --------- SEARCH - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "search_term": searchTerm - } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) - - - // --------- FETCH DATA - // fetch json of page of domains, given params - let baseUrl = document.getElementById("get_members_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no members exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const memberList = document.querySelector('#members tbody'); - memberList.innerHTML = ''; - - const UserPortfolioPermissionChoices = data.UserPortfolioPermissionChoices; - const invited = 'Invited'; - const invalid_date = 'Invalid date'; - - data.members.forEach(member => { - const member_id = member.source + member.id; - const member_name = member.name; - const member_display = member.member_display; - const member_permissions = member.permissions; - const domain_urls = member.domain_urls; - const domain_names = member.domain_names; - const num_domains = domain_urls.length; - - const last_active = this.handleLastActive(member.last_active); - - const action_url = member.action_url; - const action_label = member.action_label; - const svg_icon = member.svg_icon; - - const row = document.createElement('tr'); - - let admin_tagHTML = ``; - if (member.is_admin) - admin_tagHTML = `Admin` - - // generate html blocks for domains and permissions for the member - let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, action_url); - let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices); - - // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand - let showMoreButton = ''; - const showMoreRow = document.createElement('tr'); - if (domainsHTML || permissionsHTML) { - showMoreButton = ` - - `; - - showMoreRow.innerHTML = `
      ${domainsHTML} ${permissionsHTML}
      `; - showMoreRow.classList.add('show-more-content'); - showMoreRow.classList.add('display-none'); - showMoreRow.id = member_id; - } - - row.innerHTML = ` - - ${member_display} ${admin_tagHTML} ${showMoreButton} - - - ${last_active.display_value} - - - - - ${action_label} ${member_name} - - - `; - memberList.appendChild(row); - if (domainsHTML || permissionsHTML) { - memberList.appendChild(showMoreRow); - } - }); - - this.initShowMoreButtons(); - - // Do not scroll on first page load - if (scroll) - ScrollToElement('class', 'members'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'member', - '#members-pagination', - '#members-pagination .usa-pagination__counter', - '#members', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching members:', error)); - } -} - -class MemberDomainsTable extends LoadTableBase { - - constructor() { - super('member-domains'); - this.currentSortBy = 'name'; - } - /** - * Loads rows in the members list, as well as updates pagination around the members list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - - // --------- SEARCH - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "search_term": searchTerm, - } - ); - - let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null; - let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null; - let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null; - - if (portfolio) - searchParams.append("portfolio", portfolio) - if (emailValue) - searchParams.append("email", emailValue) - if (memberIdValue) - searchParams.append("member_id", memberIdValue) - if (memberOnly) - searchParams.append("member_only", memberOnly) - - - // --------- FETCH DATA - // fetch json of page of domais, given params - let baseUrl = document.getElementById("get_member_domains_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no members exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const memberDomainsList = document.querySelector('#member-domains tbody'); - memberDomainsList.innerHTML = ''; - - - data.domains.forEach(domain => { - const row = document.createElement('tr'); - - row.innerHTML = ` - - ${domain.name} - - `; - memberDomainsList.appendChild(row); - }); - - // Do not scroll on first page load - if (scroll) - ScrollToElement('class', 'member-domains'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'member domain', - '#member-domains-pagination', - '#member-domains-pagination .usa-pagination__counter', - '#member-domains', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domains:', error)); - } -} - - -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domains list and associated functionality. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const isDomainsPage = document.getElementById("domains") - if (isDomainsPage){ - const domainsTable = new DomainsTable(); - if (domainsTable.tableWrapper) { - // Initial load - domainsTable.loadTable(1); - } - } -}); - -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domain requests list and associated functionality. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const domainRequestsSectionWrapper = document.getElementById('domain-requests'); - if (domainRequestsSectionWrapper) { - const domainRequestsTable = new DomainRequestsTable(); - if (domainRequestsTable.tableWrapper) { - domainRequestsTable.loadTable(1); - } - } - - document.addEventListener('focusin', function(event) { - closeOpenAccordions(event); - }); - - document.addEventListener('click', function(event) { - closeOpenAccordions(event); - }); - - function closeMoreActionMenu(accordionThatIsOpen) { - if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") { - accordionThatIsOpen.click(); - } - } - - function closeOpenAccordions(event) { - const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]'); - openAccordions.forEach((openAccordionButton) => { - // Find the corresponding accordion - const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); - if (accordion && !accordion.contains(event.target)) { - // Close the accordion if the click is outside - closeMoreActionMenu(openAccordionButton); - } - }); - } -}); - -const utcDateString = (dateString) => { - const date = new Date(dateString); - const utcYear = date.getUTCFullYear(); - const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' }); - const utcDay = date.getUTCDate().toString().padStart(2, '0'); - let utcHours = date.getUTCHours(); - const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0'); - - const ampm = utcHours >= 12 ? 'PM' : 'AM'; - utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12' - - return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`; -}; - - - -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the members list and associated functionality. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const isMembersPage = document.getElementById("members") - if (isMembersPage){ - const membersTable = new MembersTable(); - if (membersTable.tableWrapper) { - // Initial load - membersTable.loadTable(1); - } - } -}); - -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the member domains list and associated functionality. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const isMemberDomainsPage = document.getElementById("member-domains") - if (isMemberDomainsPage){ - const memberDomainsTable = new MemberDomainsTable(); - if (memberDomainsTable.tableWrapper) { - // Initial load - memberDomainsTable.loadTable(1); - } - } -}); - -/** - * An IIFE that displays confirmation modal on the user profile page - */ -(function userProfileListener() { - - const showConfirmationModalTrigger = document.querySelector('.show-confirmation-modal'); - if (showConfirmationModalTrigger) { - showConfirmationModalTrigger.click(); - } -} -)(); - -/** - * An IIFE that hooks up the edit buttons on the finish-user-setup page - */ -(function finishUserSetupListener() { - - function getInputField(fieldName){ - return document.querySelector(`#id_${fieldName}`) - } - - // Shows the hidden input field and hides the readonly one - function showInputFieldHideReadonlyField(fieldName, button) { - let inputField = getInputField(fieldName) - let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`) - - readonlyField.classList.toggle('display-none'); - inputField.classList.toggle('display-none'); - - // Toggle the bold style on the grid row - let gridRow = button.closest(".grid-col-2").closest(".grid-row") - if (gridRow){ - gridRow.classList.toggle("bold-usa-label") - } - } - - function handleFullNameField(fieldName = "full_name") { - // Remove the display-none class from the nearest parent div - let nameFieldset = document.querySelector("#profile-name-group"); - if (nameFieldset){ - nameFieldset.classList.remove("display-none"); - } - - // Hide the "full_name" field - let inputField = getInputField(fieldName); - if (inputField) { - inputFieldParentDiv = inputField.closest("div"); - if (inputFieldParentDiv) { - inputFieldParentDiv.classList.add("display-none"); - } - } - } - - function handleEditButtonClick(fieldName, button){ - button.addEventListener('click', function() { - // Lock the edit button while this operation occurs - button.disabled = true - - if (fieldName == "full_name"){ - handleFullNameField(); - }else { - showInputFieldHideReadonlyField(fieldName, button); - } - - // Hide the button itself - button.classList.add("display-none"); - - // Unlock after it completes - button.disabled = false - }); - } - - function setupListener(){ - - - - document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { - // Get the "{field_name}" and "edit-button" - let fieldIdParts = button.id.split("__") - if (fieldIdParts && fieldIdParts.length > 0){ - let fieldName = fieldIdParts[0] - - // When the edit button is clicked, show the input field under it - handleEditButtonClick(fieldName, button); - - let editableFormGroup = button.parentElement.parentElement.parentElement; - if (editableFormGroup){ - let readonlyField = editableFormGroup.querySelector(".toggleable_input__readonly-field") - let inputField = document.getElementById(`id_${fieldName}`); - if (!inputField || !readonlyField) { - return; - } - - let inputFieldValue = inputField.value - if (inputFieldValue || fieldName == "full_name"){ - if (fieldName == "full_name"){ - let firstName = document.querySelector("#id_first_name"); - let middleName = document.querySelector("#id_middle_name"); - let lastName = document.querySelector("#id_last_name"); - if (firstName && lastName && firstName.value && lastName.value) { - let values = [firstName.value, middleName.value, lastName.value] - readonlyField.innerHTML = values.join(" "); - }else { - let fullNameField = document.querySelector('#full_name__edit-button-readonly'); - let svg = fullNameField.querySelector("svg use") - if (svg) { - const currentHref = svg.getAttribute('xlink:href'); - if (currentHref) { - const parts = currentHref.split('#'); - if (parts.length === 2) { - // Keep the path before '#' and replace the part after '#' with 'invalid' - const newHref = parts[0] + '#error'; - svg.setAttribute('xlink:href', newHref); - fullNameField.classList.add("toggleable_input__error") - label = fullNameField.querySelector(".toggleable_input__readonly-field") - label.innerHTML = "Unknown"; - } - } - } - } - - // Technically, the full_name field is optional, but we want to display it as required. - // This style is applied to readonly fields (gray text). This just removes it, as - // this is difficult to achieve otherwise by modifying the .readonly property. - if (readonlyField.classList.contains("text-base")) { - readonlyField.classList.remove("text-base") - } - }else { - readonlyField.innerHTML = inputFieldValue - } - } - } - } - }); - } - - function showInputOnErrorFields(){ - document.addEventListener('DOMContentLoaded', function() { - - // Get all input elements within the form - let form = document.querySelector("#finish-profile-setup-form"); - let inputs = form ? form.querySelectorAll("input") : null; - if (!inputs) { - return null; - } - - let fullNameButtonClicked = false - inputs.forEach(function(input) { - let fieldName = input.name; - let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); - - // If no error message is found, do nothing - if (!fieldName || !errorMessage) { - return null; - } - - let editButton = document.querySelector(`#${fieldName}__edit-button`); - if (editButton){ - // Show the input field of the field that errored out - editButton.click(); - } - - // If either the full_name field errors out, - // or if any of its associated fields do - show all name related fields. - let nameFields = ["first_name", "middle_name", "last_name"]; - if (nameFields.includes(fieldName) && !fullNameButtonClicked){ - // Click the full name button if any of its related fields error out - fullNameButton = document.querySelector("#full_name__edit-button"); - if (fullNameButton) { - fullNameButton.click(); - fullNameButtonClicked = true; - } - } - }); - }); - }; - - setupListener(); - - // Show the input fields if an error exists - showInputOnErrorFields(); - -})(); - - -/** - * An IIFE that changes the default clear behavior on comboboxes to the input field. - * We want the search bar to act soley as a search bar. - */ -(function loadInitialValuesForComboBoxes() { - var overrideDefaultClearButton = true; - var isTyping = false; - - document.addEventListener('DOMContentLoaded', (event) => { - handleAllComboBoxElements(); - }); - - function handleAllComboBoxElements() { - const comboBoxElements = document.querySelectorAll(".usa-combo-box"); - comboBoxElements.forEach(comboBox => { - const input = comboBox.querySelector("input"); - const select = comboBox.querySelector("select"); - if (!input || !select) { - console.warn("No combobox element found"); - return; - } - // Set the initial value of the combobox - let initialValue = select.getAttribute("data-default-value"); - let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input"); - if (!clearInputButton) { - console.warn("No clear element found"); - return; - } - - // Override the default clear button behavior such that it no longer clears the input, - // it just resets to the data-initial-value. - - // Due to the nature of how uswds works, this is slightly hacky. - - // Use a MutationObserver to watch for changes in the dropdown list - const dropdownList = comboBox.querySelector(`#${input.id}--list`); - const observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.type === "childList") { - addBlankOption(clearInputButton, dropdownList, initialValue); - } - }); - }); - - // Configure the observer to watch for changes in the dropdown list - const config = { childList: true, subtree: true }; - observer.observe(dropdownList, config); - - // Input event listener to detect typing - input.addEventListener("input", () => { - isTyping = true; - }); - - // Blur event listener to reset typing state - input.addEventListener("blur", () => { - isTyping = false; - }); - - // Hide the reset button when there is nothing to reset. - // Do this once on init, then everytime a change occurs. - updateClearButtonVisibility(select, initialValue, clearInputButton) - select.addEventListener("change", () => { - updateClearButtonVisibility(select, initialValue, clearInputButton) - }); - - // Change the default input behaviour - have it reset to the data default instead - clearInputButton.addEventListener("click", (e) => { - if (overrideDefaultClearButton && initialValue) { - e.preventDefault(); - e.stopPropagation(); - input.click(); - // Find the dropdown option with the desired value - const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option"); - if (dropdownOptions) { - dropdownOptions.forEach(option => { - if (option.getAttribute("data-value") === initialValue) { - // Simulate a click event on the dropdown option - option.click(); - } - }); - } - } - }); - }); - } - - function updateClearButtonVisibility(select, initialValue, clearInputButton) { - if (select.value === initialValue) { - hideElement(clearInputButton); - }else { - showElement(clearInputButton) - } - } - - function addBlankOption(clearInputButton, dropdownList, initialValue) { - if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) { - const blankOption = document.createElement("li"); - blankOption.setAttribute("role", "option"); - blankOption.setAttribute("data-value", ""); - blankOption.classList.add("usa-combo-box__list-option"); - if (!initialValue){ - blankOption.classList.add("usa-combo-box__list-option--selected") - } - blankOption.textContent = "⎯"; - - dropdownList.insertBefore(blankOption, dropdownList.firstChild); - blankOption.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - overrideDefaultClearButton = false; - // Trigger the default clear behavior - clearInputButton.click(); - overrideDefaultClearButton = true; - }); - } - } -})(); - -/** An IIFE that intializes the requesting entity page. - * This page has a radio button that dynamically toggles some fields - * Within that, the dropdown also toggles some additional form elements. -*/ -(function handleRequestingEntityFieldset() { - // Sadly, these ugly ids are the auto generated with this prefix - const formPrefix = "portfolio_requesting_entity" - const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`); - const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`); - const select = document.getElementById(`id_${formPrefix}-sub_organization`); - const suborgContainer = document.getElementById("suborganization-container"); - const suborgDetailsContainer = document.getElementById("suborganization-container__details"); - if (!radios || !select || !suborgContainer || !suborgDetailsContainer) return; - - // requestingSuborganization: This just broadly determines if they're requesting a suborg at all - // requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not. - var requestingSuborganization = Array.from(radios).find(radio => radio.checked)?.value === "True"; - var requestingNewSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`); - - function toggleSuborganization(radio=null) { - if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True"; - requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer); - requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False"; - requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer); - } - - // Add fake "other" option to sub_organization select - if (select && !Array.from(select.options).some(option => option.value === "other")) { - select.add(new Option("Other (enter your organization manually)", "other")); - } - - if (requestingNewSuborganization.value === "True") { - select.value = "other"; - } - - // Add event listener to is_suborganization radio buttons, and run for initial display - toggleSuborganization(); - radios.forEach(radio => { - radio.addEventListener("click", () => toggleSuborganization(radio)); - }); - - // Add event listener to the suborg dropdown to show/hide the suborg details section - select.addEventListener("change", () => toggleSuborganization()); -})(); +(()=>{"use strict";var e={635:(e,t,n)=>{function a(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(e){if("string"==typeof e)return r(e,t);var n={}.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?r(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var a=0,o=function(){};return{s:o,n:function(){return a>=e.length?{done:!0}:{done:!1,value:e[a++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,l=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){l=!0,i=e},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw i}}}}function r(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,a=Array(t);ng});var o="Please check this field for errors.",i="error",s="success";function l(e,t){var n=document.getElementById(e+"-live-region");n||(n=function(e){var t=document.createElement("div");return t.setAttribute("role","region"),t.setAttribute("aria-live","polite"),t.setAttribute("id",e+"-live-region"),t.classList.add("usa-sr-only"),document.body.appendChild(t),t}(e)),n.innerHTML=t}function c(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:o;t?(e.setCustomValidity(""),e.removeAttribute("aria-invalid"),e.classList.remove("usa-input--error")):(e.classList.remove("usa-input--success"),e.setAttribute("aria-invalid","true"),e.setCustomValidity(n),e.classList.add("usa-input--error"))}function u(e,t,n,a){if(e.id||t){var r=document.getElementById((e.id||t)+"--toast");if(n)if(r)r.className="usa-alert usa-alert--".concat(n," usa-alert--slim"),r.querySelector("div p").innerHTML=a,function(e){e.style.position="relative",e.style.left="unset",e.style.visibility="visible"}(r);else{r=document.createElement("div");var o=document.createElement("div"),i=document.createElement("p");r.setAttribute("id",(e.id||t)+"--toast"),r.className="usa-alert usa-alert--".concat(n," usa-alert--slim"),o.classList.add("usa-alert__body"),i.classList.add("usa-alert__text"),i.innerHTML=a,o.appendChild(i),r.appendChild(o),e.parentNode.insertBefore(r,e.nextSibling)}else r&&function(e){e.style.position="absolute",e.style.left="-100vw",e.style.visibility="hidden"}(r)}else console.error("Elements must have an `id` to show an inline toast.")}function d(e){!function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"/api/v1/",a=new XMLHttpRequest;a.open("GET",n+e),a.send(),a.onload=function(){200==a.status&&t(JSON.parse(a.response))}}("available/?domain=".concat(e.value),(function(t){c(e,t&&t.available,msg=t.message),l(e.id,t.message),ignore_blank=e.classList.contains("blank-ok"),e.validity.valid?(e.classList.add("usa-input--success"),u(e.parentElement,e.id,s,t.message)):ignore_blank&&"required"==t.code?(error="usa-input--error",e.classList.contains(error)&&e.classList.remove(error)):u(e.parentElement,e.id,i,t.message)}))}function m(e){e.classList.remove("usa-input--success"),l(e.id,""),u(e.parentElement,e.id)}function f(e){var t=e.getAttribute("validate")||"";if(t.length){var n,r=a(t.split(" "));try{for(r.s();!(n=r.n()).done;)"domain"===n.value&&d(e)}catch(e){r.e(e)}finally{r.f()}c(e,!0)}}function p(e){!function(e){var t=e.getAttribute("validate")||"";if(t.length){var n,r=a(t.split(" "));try{for(r.s();!(n=r.n()).done;)"domain"===n.value&&m(e)}catch(e){r.e(e)}finally{r.f()}c(e,!0)}}(e.target),e.target.hasAttribute("auto-validate")&&f(e.target)}function h(e){var t=e.target.getAttribute("validate-for")||"";if(t.length){var n=document.getElementById(t);v(n,!0),f(n)}}function v(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=document.getElementById("".concat(e.id,"__error-message"));if(n){n.remove(),e.classList.contains("usa-input--error")&&e.classList.remove("usa-input--error");var r=document.querySelector('label[for="'.concat(e.id,'"]'));if(r){r.classList.remove("usa-label--error");var o=r.parentElement;o&&o.classList.remove("usa-form-group--error")}if(t){var i,s=a(document.querySelectorAll(".usa-alert--error"));try{for(s.s();!(i=s.n()).done;){var l=i.value;l.id!=="".concat(e.id,"--toast")&&l.remove()}}catch(e){s.e(e)}finally{s.f()}}}}function g(){var e,t=a(document.querySelectorAll("[validate]"));try{for(t.s();!(e=t.n()).done;)e.value.addEventListener("input",p)}catch(e){t.e(e)}finally{t.f()}var n,r=document.getElementById("validate-alt-domains-availability"),o=a(document.querySelectorAll("[validate-for]"));try{for(o.s();!(n=o.n()).done;){var i=n.value;i===r?i.addEventListener("click",(function(e){var t,n;t=r,(n=Array.from(document.querySelectorAll(".repeatable-form input"))).forEach((function(e){v(e,!0),f(e)})),n=n.map((function(e){return e.id})).join(", "),t.setAttribute("validate-for",n)})):i.addEventListener("click",h)}}catch(e){o.e(e)}finally{o.f()}}},686:(e,t,n)=>{},790:(e,t,n)=>{var a=n(178);function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}function o(e,t,n){return t=s(t),function(e,t){if(t&&("object"==r(t)||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}(e,i()?Reflect.construct(t,n||[],s(e).constructor):t.apply(e,n))}function i(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(e){}return(i=function(){return!!e})()}function s(e){return s=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},s(e)}function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&c(e,t)}function c(e,t){return c=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},c(e,t)}function u(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function d(e,t){for(var n=0;n\s*<"),n.insertBefore(d,a),d.style.display="block",d.querySelectorAll("input").forEach((function(e){e.classList.remove("usa-input--error"),e.classList.remove("usa-input--success"),"text"===e.type||"number"===e.type||"password"===e.type||"email"===e.type||"tel"===e.type?e.value="":"checkbox"!==e.type&&"radio"!==e.type||(e.checked=!1)})),d.querySelectorAll("select").forEach((function(e){e.classList.remove("usa-input--error"),e.classList.remove("usa-input--success"),e.selectedIndex=0})),d.querySelectorAll("label").forEach((function(e){e.classList.remove("usa-label--error"),e.classList.remove("usa-label--success")})),d.querySelectorAll(".usa-form-group").forEach((function(e){e.classList.remove("usa-form-group--error"),e.classList.remove("usa-form-group--success")})),d.querySelectorAll(".usa-error-message, .usa-alert").forEach((function(e){var t=e.closest("div");t&&t.remove()})),c.setAttribute("value","".concat(u));var _=d.querySelector(".delete-record");_&&function(e,t){var n="form",a=document.querySelector(".nameservers-form"),r=document.querySelector(".other-contacts-form"),o=document.querySelector("#add-form");r?(n="other_contacts",e.addEventListener("click",(function(e){y(e,t)}))):e.addEventListener("click",(function(e){b(e,t,a,o,n)}))}(_,r),o&&13==u&&a.setAttribute("disabled","true"),o&&l.length>=2&&l.forEach((function(e,t){Array.from(e.querySelectorAll(".delete-record")).forEach((function(e){e.removeAttribute("disabled")}))}))}))}(),function(){if(document.querySelector("#save-ds-data"))var e=0,t=setInterval((function(){++e>100&&clearInterval(t);var n=document.querySelector("#ds-toggle-dnssec-alert");n&&(n.click(),clearInterval(t))}),50)}(),v("other_contacts-has_other_contacts","other-employees","no-other-employees"),v("additional_details-has_anything_else_text","anything-else",null),g("member_access_level",{admin:"new-member-admin-permissions",basic:"new-member-basic-permissions"}),function(){if(document.querySelector(".nameservers-form")){var e=document.querySelectorAll(".repeatable-form");e.length<3&&e.forEach((function(e){Array.from(e.querySelectorAll(".delete-record")).forEach((function(e){e.setAttribute("disabled","true")}))}))}}(),function(){if(document.querySelector(".nameservers-form")){var e=document.querySelectorAll(".repeatable-form");e.length<3&&e.forEach((function(e){Array.from(e.querySelectorAll(".delete-record")).forEach((function(e){e.setAttribute("disabled","true")}))}))}}(),v("additional_details-has_cisa_representative","cisa-representative",null);var _=function(){return m((function e(t){u(this,e),f(this,"updateDisplay",(function(e,t,n,r){var o=e.unfiltered_total,i=e.total;o?i?((0,a.kl)(t),(0,a.Bt)(r),(0,a.Bt)(n)):((0,a.Bt)(t),(0,a.kl)(r),(0,a.Bt)(n)):((0,a.Bt)(t),(0,a.Bt)(r),(0,a.kl)(n))})),f(this,"unsetHeader",(function(e){e.removeAttribute("aria-sort");var t=e.innerText,n="".concat(t,', sortable column, currently unsorted"');e.setAttribute("aria-label",n),e.querySelector(".usa-table__header__button").setAttribute("title","Click to sort by ascending order.")})),this.tableWrapper=document.getElementById("".concat(t,"__table-wrapper")),this.tableHeaders=document.querySelectorAll("#".concat(t," th[data-sortable]")),this.currentSortBy="id",this.currentOrder="asc",this.currentStatus=[],this.currentSearchTerm="",this.scrollToTable=!1,this.searchInput=document.getElementById("".concat(t,"__search-field")),this.searchSubmit=document.getElementById("".concat(t,"__search-field-submit")),this.tableAnnouncementRegion=document.getElementById("".concat(t,"__usa-table__announcement-region")),this.resetSearchButton=document.getElementById("".concat(t,"__reset-search")),this.resetFiltersButton=document.getElementById("".concat(t,"__reset-filters")),this.statusCheckboxes=document.querySelectorAll(".".concat(t,' input[name="filter-status"]')),this.statusIndicator=document.getElementById("".concat(t,"__filter-indicator")),this.statusToggle=document.getElementById("".concat(t,"__usa-button--filter")),this.noTableWrapper=document.getElementById("".concat(t,"__no-data")),this.noSearchResultsWrapper=document.getElementById("".concat(t,"__no-search-results")),this.portfolioElement=document.getElementById("portfolio-js-value"),this.portfolioValue=this.portfolioElement?this.portfolioElement.getAttribute("data-portfolio"):null,this.initializeTableHeaders(),this.initializeSearchHandler(),this.initializeStatusToggleHandler(),this.initializeFilterCheckboxes(),this.initializeResetSearchButton(),this.initializeResetFiltersButton(),this.initializeAccordionAccessibilityListeners()}),[{key:"updatePagination",value:function(e,t,n,a,r,o,i,s,l){var c=this,u=document.querySelector("".concat(t," .usa-pagination__list")),d=document.querySelector(n),m=document.querySelector(t);if(d.innerHTML="",u.innerHTML="",u.classList.toggle("display-none",o<=1),m.classList.toggle("display-none",l<1),d.innerHTML="".concat(l," ").concat(e).concat(l>1?"s":"").concat(this.currentSearchTerm?' for "'+this.currentSearchTerm+'"':""),i){var f=document.createElement("li");f.className="usa-pagination__item usa-pagination__arrow",f.innerHTML='\n \n \n Previous\n \n '),f.querySelector("a").addEventListener("click",(function(e){e.preventDefault(),c.loadTable(r-1)})),u.appendChild(f)}if(r>2&&(u.appendChild(this.createPageItem(1,a,r)),r>3)){var p=document.createElement("li");p.className="usa-pagination__item usa-pagination__overflow",p.setAttribute("aria-label","ellipsis indicating non-visible pages"),p.innerHTML="",u.appendChild(p)}for(var h=Math.max(1,r-1);h<=Math.min(o,r+1);h++)u.appendChild(this.createPageItem(h,a,r));if(r\n Next\n \n \n '),g.querySelector("a").addEventListener("click",(function(e){e.preventDefault(),c.loadTable(r+1)})),u.appendChild(g)}}},{key:"createPageItem",value:function(e,t,n){var a=this,r=document.createElement("li");return r.className="usa-pagination__item usa-pagination__page-no",r.innerHTML='\n ').concat(e,"\n "),e===n&&(r.querySelector("a").classList.add("usa-current"),r.querySelector("a").setAttribute("aria-current","page")),r.querySelector("a").addEventListener("click",(function(t){t.preventDefault(),a.loadTable(e)})),r}},{key:"loadTable",value:function(e,t,n){throw new Error("loadData() must be implemented in a subclass")}},{key:"initializeTableHeaders",value:function(){var e=this;this.tableHeaders.forEach((function(t){t.addEventListener("click",(function(){var n=t.getAttribute("data-sortable"),a="asc";n===e.currentSortBy&&(a="asc"===e.currentOrder?"desc":"asc"),e.loadTable(1,n,a)}))}))}},{key:"initializeSearchHandler",value:function(){var e=this;this.searchSubmit.addEventListener("click",(function(t){t.preventDefault(),e.currentSearchTerm=e.searchInput.value,e.currentSearchTerm?(0,a.kl)(e.resetSearchButton):(0,a.Bt)(e.resetSearchButton),e.loadTable(1,"id","asc"),e.resetHeaders()}))}},{key:"initializeStatusToggleHandler",value:function(){var e=this;this.statusToggle&&this.statusToggle.addEventListener("click",(function(){var t;"/public/img/sprite.svg#expand_more"===(t=e.statusToggle.querySelector("use")).getAttribute("xlink:href")?t.setAttribute("xlink:href","/public/img/sprite.svg#expand_less"):t.setAttribute("xlink:href","/public/img/sprite.svg#expand_more")}))}},{key:"initializeFilterCheckboxes",value:function(){var e=this;this.statusCheckboxes.forEach((function(t){t.addEventListener("change",(function(){var n=t.value;if(t.checked)e.currentStatus.push(n);else{var r=e.currentStatus.indexOf(n);r>-1&&e.currentStatus.splice(r,1)}0==e.currentStatus.length?(0,a.Bt)(e.resetFiltersButton):(0,a.kl)(e.resetFiltersButton),e.scrollToTable=!1,e.loadTable(1,"id","asc"),e.resetHeaders(),e.updateStatusIndicator()}))}))}},{key:"resetHeaders",value:function(){var e=this;this.tableHeaders.forEach((function(t){e.unsetHeader(t)})),this.tableAnnouncementRegion.innerHTML=""}},{key:"resetSearch",value:function(){this.searchInput.value="",this.currentSearchTerm="",(0,a.Bt)(this.resetSearchButton),this.loadTable(1,"id","asc"),this.resetHeaders()}},{key:"initializeResetSearchButton",value:function(){var e=this;this.resetSearchButton&&this.resetSearchButton.addEventListener("click",(function(){e.resetSearch()}))}},{key:"resetFilters",value:function(){this.currentStatus=[],this.statusCheckboxes.forEach((function(e){e.checked=!1})),(0,a.Bt)(this.resetFiltersButton),this.scrollToTable=!1,this.loadTable(1,"id","asc"),this.resetHeaders(),this.updateStatusIndicator()}},{key:"initializeResetFiltersButton",value:function(){var e=this;this.resetFiltersButton&&this.resetFiltersButton.addEventListener("click",(function(){e.resetFilters()}))}},{key:"updateStatusIndicator",value:function(){this.statusIndicator.innerHTML="",(0,a.Bt)(this.statusIndicator),this.currentStatus.length&&(this.statusIndicator.innerHTML="("+this.currentStatus.length+")"),(0,a.kl)(this.statusIndicator)}},{key:"closeFilters",value:function(){"true"===this.statusToggle.getAttribute("aria-expanded")&&this.statusToggle.click()}},{key:"initializeAccordionAccessibilityListeners",value:function(){var e=this;document.addEventListener("focusin",(function(t){var n=document.querySelector(".usa-accordion--select");document.querySelector('.usa-button--filter[aria-expanded="true"]')&&!n.contains(t.target)&&e.closeFilters()})),document.addEventListener("click",(function(t){var n=document.querySelector(".usa-accordion--select");document.querySelector('.usa-button--filter[aria-expanded="true"]')&&!n.contains(t.target)&&e.closeFilters()}))}}])}(),S=function(e){function t(){return u(this,t),o(this,t,["domains"])}return l(t,e),m(t,[{key:"loadTable",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.currentSortBy,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.currentOrder,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:this.scrollToTable,o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:this.currentStatus,i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:this.currentSearchTerm,s=arguments.length>6&&void 0!==arguments[6]?arguments[6]:this.portfolioValue,l=document.getElementById("get_domains_json_url");if(l){var c=l.innerHTML;if(c){var u=new URLSearchParams({page:e,sort_by:n,order:a,status:o,search_term:i});s&&u.append("portfolio",s);var d="".concat(c,"?").concat(u.toString());fetch(d).then((function(e){return e.json()})).then((function(e){if(e.error)console.error("Error in AJAX call: "+e.error);else{t.updateDisplay(e,t.tableWrapper,t.noTableWrapper,t.noSearchResultsWrapper,t.currentSearchTerm);var o=document.querySelector("#domains tbody");o.innerHTML="",e.domains.forEach((function(e){var n=e.expiration_date?new Date(e.expiration_date):null,a=n?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):"",r=n?n.getTime():"",i=e.action_url,s=e.domain_info__sub_organization?e.domain_info__sub_organization:"⎯",l=document.createElement("tr"),c="";t.portfolioValue&&(c='\n \n ').concat(s,"\n \n ")),l.innerHTML='\n \n '.concat(e.name,'\n \n \n ').concat(a,'\n \n \n ').concat(e.state_display,'\n \n \n \n \n ').concat(c,'\n \n \n \n ').concat(e.action_label,' ').concat(e.name,"\n \n \n "),o.appendChild(l)})),function e(){window.tooltip&&"function"==typeof window.tooltip.init?window.tooltip.init():setTimeout(e,100)}(),r&&h("class","domains"),t.scrollToTable=!0,t.updatePagination("domain","#domains-pagination","#domains-pagination .usa-pagination__counter","#domains",e.page,e.num_pages,e.has_previous,e.has_next,e.total),t.currentSortBy=n,t.currentOrder=a,t.currentSearchTerm=i}})).catch((function(e){return console.error("Error fetching domains:",e)}))}}}}])}(_),E=function(e){function t(){return u(this,t),o(this,t,["domain-requests"])}return l(t,e),m(t,[{key:"toggleExportButton",value:function(e){var t=document.getElementById("export-csv");t&&(e.length>0?(0,a.kl)(t):(0,a.Bt)(t))}},{key:"loadTable",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.currentSortBy,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.currentOrder,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:this.scrollToTable,o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:this.currentStatus,i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:this.currentSearchTerm,s=arguments.length>6&&void 0!==arguments[6]?arguments[6]:this.portfolioValue,l=document.getElementById("get_domain_requests_json_url");if(l){var c=l.innerHTML;if(c){var u=new URLSearchParams({page:e,sort_by:n,order:a,status:o,search_term:i});s&&u.append("portfolio",s);var d="".concat(c,"?").concat(u.toString());fetch(d).then((function(e){return e.json()})).then((function(e){if(e.error)console.error("Error in AJAX call: "+e.error);else{t.toggleExportButton(e.domain_requests),t.updateDisplay(e,t.tableWrapper,t.noTableWrapper,t.noSearchResultsWrapper,t.currentSearchTerm);var o,s=document.querySelector("#domain-requests tbody");s.innerHTML="",document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]').forEach((function(e){e.remove()})),window.modal.off(),o=e.domain_requests.some((function(e){return e.is_deletable}));var l=document.querySelector(".delete-header");if(o){if(!l){var c=document.createElement("th");c.setAttribute("scope","col"),c.setAttribute("role","columnheader"),c.setAttribute("class","delete-header"),c.innerHTML='\n Delete Action',document.querySelector("#domain-requests thead tr").appendChild(c)}}else l&&l.remove();e.domain_requests.forEach((function(e){var n=e.requested_domain?e.requested_domain:'New domain request
      ('.concat(A(e.created_at),")"),a=e.action_url,r=e.action_label,i=e.last_submitted_date?new Date(e.last_submitted_date).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):'Not submitted',l='\n Domain request cannot be deleted now. Edit the request for more information.',c="";if(t.portfolioValue&&(c='\n \n '.concat(e.creator?e.creator:"","\n \n ")),e.is_deletable){var u="",d="";e.requested_domain?(u="Are you sure you want to delete ".concat(e.requested_domain,"?"),d="This will remove the domain request from the .gov registrar. This action cannot be undone."):e.created_at?(u="Are you sure you want to delete this domain request?",d="This will remove the domain request (created ".concat(A(e.created_at),") from the .gov registrar. This action cannot be undone")):(u="Are you sure you want to delete New domain request?",d="This will remove the domain request from the .gov registrar. This action cannot be undone."),l='\n \n Delete ').concat(n,"\n ");var m='\n \n '),f=document.createElement("div");f.setAttribute("class","usa-modal"),f.setAttribute("id","toggle-delete-domain-alert-".concat(e.id)),f.setAttribute("aria-labelledby","Are you sure you want to continue?"),f.setAttribute("aria-describedby","Domain will be removed"),f.setAttribute("data-force-action",""),f.innerHTML='\n
      \n
      \n \n
      \n \n
      \n \n
      \n \n \n \n
      \n '),t.tableWrapper.appendChild(f),t.portfolioValue&&(l='\n \n Delete ').concat(n,'\n \n\n
      \n
      \n
      \n \n
      \n "))}var p=document.createElement("tr");p.innerHTML='\n \n '.concat(n,'\n \n \n ').concat(i,"\n \n ").concat(c,'\n \n ').concat(e.status,'\n \n \n \n \n ').concat(r,' ').concat(e.requested_domain?e.requested_domain:"New domain request","\n \n \n ").concat(o?""+l+"":"","\n "),s.appendChild(p)})),window.modal.on(),document.querySelectorAll(".usa-modal__content").forEach((function(n){var a=n.querySelector(".usa-modal__submit"),r=n.querySelector(".usa-modal__close");a.addEventListener("click",(function(){var n=a.getAttribute("data-pk");r.click();var o=e.page;1==e.total&&e.unfiltered_total>1&&o--,t.deleteDomainRequest(n,o)}))})),r&&h("class","domain-requests"),t.scrollToTable=!0,t.updatePagination("domain request","#domain-requests-pagination","#domain-requests-pagination .usa-pagination__counter","#domain-requests",e.page,e.num_pages,e.has_previous,e.has_next,e.total),t.currentSortBy=n,t.currentOrder=a,t.currentSearchTerm=i}})).catch((function(e){return console.error("Error fetching domain requests:",e)}))}}}},{key:"deleteDomainRequest",value:function(e,t){var n=this,a=document.querySelector('input[name="csrfmiddlewaretoken"]').value,r="csrfmiddlewaretoken=".concat(encodeURIComponent(a),"&delete-domain-request=");fetch("/domain-request/".concat(e,"/delete"),{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","X-CSRFToken":a},body:r}).then((function(e){if(!e.ok)throw new Error("HTTP error! status: ".concat(e.status));n.loadTable(t,n.currentSortBy,n.currentOrder,n.scrollToTable,n.currentSearchTerm)})).catch((function(e){return console.error("Error fetching domain requests:",e)}))}}])}(_),T=function(e){function t(){return u(this,t),o(this,t,["members"])}return l(t,e),m(t,[{key:"initShowMoreButtons",value:function(){document.querySelectorAll(".usa-button--show-more-button").forEach((function(e){var t=e.dataset.for,n=document.getElementById(t),r=e.parentElement.parentElement;n&&"tr"===n.tagName.toLowerCase()&&n.classList.contains("show-more-content")&&r&&"tr"===r.tagName.toLowerCase()?e.addEventListener("click",(function(){!function(e,t,n){var r=e.querySelector("span"),o=e.querySelector("use");t.classList.contains("display-none")?((0,a.kl)(t),r.textContent="Close",o.setAttribute("xlink:href","/public/img/sprite.svg#expand_less"),n.classList.add("hide-td-borders"),e.setAttribute("aria-label","Close additional information")):((0,a.Bt)(t),r.textContent="Expand",o.setAttribute("xlink:href","/public/img/sprite.svg#expand_more"),n.classList.remove("hide-td-borders"),e.setAttribute("aria-label","Expand for additional information"))}(e,n,r)})):console.warn("Found a toggle button with no associated toggleable content or parent row")}))}},{key:"handleLastActive",value:function(e){var t="Invited",n="Invalid date",a=n,r=-1;if(e===t)a=t,r=0;else if(e&&e!==n){var o=new Date(e);isNaN(o.getTime())?console.error("Error: Invalid date string provided: ".concat(e)):(a=o.toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"}),r=o.getTime())}return{display_value:a,sort_value:r}}},{key:"generateDomainsHTML",value:function(e,t,n,a){var r="";if(e>0){r+="
      ",r+="

      Domains assigned

      ",r+="

      This member is assigned to ".concat(e," domains:

      "),r+="",e>=6&&(r+='

      View assigned domains

      ')),r+="
      "}return r}},{key:"generatePermissionsHTML",value:function(e,t){var n="";return e.includes(t.VIEW_ALL_DOMAINS)?n+="

      Domains: Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).

      ":e.includes(t.VIEW_MANAGED_DOMAINS)&&(n+="

      Domains: Can manage domains they are assigned to and edit information about the domain (including DNS settings).

      "),e.includes(t.EDIT_REQUESTS)?n+="

      Domain requests: Can view all organization domain requests. Can create domain requests and modify their own requests.

      ":e.includes(t.VIEW_ALL_REQUESTS)&&(n+="

      Domain requests (view-only): Can view all organization domain requests. Can't create or modify any domain requests.

      "),e.includes(t.EDIT_MEMBERS)?n+="

      Members: Can manage members including inviting new members, removing current members, and assigning domains to members.

      ":e.includes(t.VIEW_MEMBERS)&&(n+="

      Members (view-only): Can view all organizational members. Can't manage any members.

      "),n||(n+="

      No additional permissions: There are no additional permissions for this member.

      "),"

      Additional permissions for this member

      "+n+"
      "}},{key:"loadTable",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.currentSortBy,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.currentOrder,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:this.scrollToTable,o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:this.currentSearchTerm,i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:this.portfolioValue,s=new URLSearchParams({page:e,sort_by:n,order:a,search_term:o});i&&s.append("portfolio",i);var l=document.getElementById("get_members_json_url");if(l){var c=l.innerHTML;if(c){var u="".concat(c,"?").concat(s.toString());fetch(u).then((function(e){return e.json()})).then((function(e){if(e.error)console.error("Error in AJAX call: "+e.error);else{t.updateDisplay(e,t.tableWrapper,t.noTableWrapper,t.noSearchResultsWrapper,t.currentSearchTerm);var i=document.querySelector("#members tbody");i.innerHTML="";var s=e.UserPortfolioPermissionChoices;e.members.forEach((function(e){var n=e.source+e.id,a=e.name,r=e.member_display,o=e.permissions,l=e.domain_urls,c=e.domain_names,u=l.length,d=t.handleLastActive(e.last_active),m=e.action_url,f=e.action_label,p=e.svg_icon,h=document.createElement("tr"),v="";e.is_admin&&(v='Admin');var g=t.generateDomainsHTML(u,c,l,m),b=t.generatePermissionsHTML(o,s),y="",_=document.createElement("tr");(g||b)&&(y='\n \n '),_.innerHTML="
      ').concat(g," ").concat(b,"
      "),_.classList.add("show-more-content"),_.classList.add("display-none"),_.id=n),h.innerHTML='\n \n ").concat(r," ").concat(v," ").concat(y,'\n \n \n ').concat(d.display_value,'\n \n \n \n \n ').concat(f,' ').concat(a,"\n \n \n "),i.appendChild(h),(g||b)&&i.appendChild(_)})),t.initShowMoreButtons(),r&&h("class","members"),t.scrollToTable=!0,t.updatePagination("member","#members-pagination","#members-pagination .usa-pagination__counter","#members",e.page,e.num_pages,e.has_previous,e.has_next,e.total),t.currentSortBy=n,t.currentOrder=a,t.currentSearchTerm=o}})).catch((function(e){return console.error("Error fetching members:",e)}))}}}}])}(_),q=function(e){function t(){var e;return u(this,t),(e=o(this,t,["member-domains"])).currentSortBy="name",e}return l(t,e),m(t,[{key:"loadTable",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.currentSortBy,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.currentOrder,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:this.scrollToTable,o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:this.currentSearchTerm,i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:this.portfolioValue,s=new URLSearchParams({page:e,sort_by:n,order:a,search_term:o}),l=this.portfolioElement?this.portfolioElement.getAttribute("data-email"):null,c=this.portfolioElement?this.portfolioElement.getAttribute("data-member-id"):null,u=this.portfolioElement?this.portfolioElement.getAttribute("data-member-only"):null;i&&s.append("portfolio",i),l&&s.append("email",l),c&&s.append("member_id",c),u&&s.append("member_only",u);var d=document.getElementById("get_member_domains_json_url");if(d){var m=d.innerHTML;if(m){var f="".concat(m,"?").concat(s.toString());fetch(f).then((function(e){return e.json()})).then((function(e){if(e.error)console.error("Error in AJAX call: "+e.error);else{t.updateDisplay(e,t.tableWrapper,t.noTableWrapper,t.noSearchResultsWrapper,t.currentSearchTerm);var i=document.querySelector("#member-domains tbody");i.innerHTML="",e.domains.forEach((function(e){var t=document.createElement("tr");t.innerHTML='\n \n '.concat(e.name,"\n \n "),i.appendChild(t)})),r&&h("class","member-domains"),t.scrollToTable=!0,t.updatePagination("member domain","#member-domains-pagination","#member-domains-pagination .usa-pagination__counter","#member-domains",e.page,e.num_pages,e.has_previous,e.has_next,e.total),t.currentSortBy=n,t.currentOrder=a,t.currentSearchTerm=o}})).catch((function(e){return console.error("Error fetching domains:",e)}))}}}}])}(_);document.addEventListener("DOMContentLoaded",(function(){if(document.getElementById("domains")){var e=new S;e.tableWrapper&&e.loadTable(1)}})),document.addEventListener("DOMContentLoaded",(function(){if(document.getElementById("domain-requests")){var e=new E;e.tableWrapper&&e.loadTable(1)}function t(e){document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]').forEach((function(t){var n,a=t.closest(".usa-accordion--more-actions");a&&!a.contains(e.target)&&"true"===(n=t).getAttribute("aria-expanded")&&n.click()}))}document.addEventListener("focusin",(function(e){t(e)})),document.addEventListener("click",(function(e){t(e)}))}));var L,A=function(e){var t=new Date(e),n=t.getUTCFullYear(),a=t.toLocaleString("en-US",{month:"short",timeZone:"UTC"}),r=t.getUTCDate().toString().padStart(2,"0"),o=t.getUTCHours(),i=t.getUTCMinutes().toString().padStart(2,"0"),s=o>=12?"PM":"AM";return o=o%12||12,"".concat(a," ").concat(r,", ").concat(n,", ").concat(o,":").concat(i," ").concat(s," UTC")};document.addEventListener("DOMContentLoaded",(function(){if(document.getElementById("members")){var e=new T;e.tableWrapper&&e.loadTable(1)}})),document.addEventListener("DOMContentLoaded",(function(){if(document.getElementById("member-domains")){var e=new q;e.tableWrapper&&e.loadTable(1)}})),(L=document.querySelector(".show-confirmation-modal"))&&L.click(),function(){function e(e){return document.querySelector("#id_".concat(e))}document.querySelectorAll('[id$="__edit-button"]').forEach((function(t){var n=t.id.split("__");if(n&&n.length>0){var a=n[0];!function(t,n){n.addEventListener("click",(function(){n.disabled=!0,"full_name"==t?function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"full_name",n=document.querySelector("#profile-name-group");n&&n.classList.remove("display-none");var a=e(t);a&&(inputFieldParentDiv=a.closest("div"),inputFieldParentDiv&&inputFieldParentDiv.classList.add("display-none"))}():function(t,n){var a=e(t);document.querySelector("#".concat(t,"__edit-button-readonly")).classList.toggle("display-none"),a.classList.toggle("display-none");var r=n.closest(".grid-col-2").closest(".grid-row");r&&r.classList.toggle("bold-usa-label")}(t,n),n.classList.add("display-none"),n.disabled=!1}))}(a,t);var r=t.parentElement.parentElement.parentElement;if(r){var o=r.querySelector(".toggleable_input__readonly-field"),i=document.getElementById("id_".concat(a));if(!i||!o)return;var s=i.value;if(s||"full_name"==a)if("full_name"==a){var l=document.querySelector("#id_first_name"),c=document.querySelector("#id_middle_name"),u=document.querySelector("#id_last_name");if(l&&u&&l.value&&u.value){var d=[l.value,c.value,u.value];o.innerHTML=d.join(" ")}else{var m=document.querySelector("#full_name__edit-button-readonly"),f=m.querySelector("svg use");if(f){var p=f.getAttribute("xlink:href");if(p){var h=p.split("#");if(2===h.length){var v=h[0]+"#error";f.setAttribute("xlink:href",v),m.classList.add("toggleable_input__error"),label=m.querySelector(".toggleable_input__readonly-field"),label.innerHTML="Unknown"}}}}o.classList.contains("text-base")&&o.classList.remove("text-base")}else o.innerHTML=s}}})),document.addEventListener("DOMContentLoaded",(function(){var e=document.querySelector("#finish-profile-setup-form"),t=e?e.querySelectorAll("input"):null;if(!t)return null;var n=!1;t.forEach((function(e){var t=e.name,a=document.querySelector("#id_".concat(t,"__error-message"));if(!t||!a)return null;var r=document.querySelector("#".concat(t,"__edit-button"));r&&r.click(),["first_name","middle_name","last_name"].includes(t)&&!n&&(fullNameButton=document.querySelector("#full_name__edit-button"),fullNameButton&&(fullNameButton.click(),n=!0))}))}))}(),function(){var e=!0,t=!1;function n(e,t,n){e.value===t?(0,a.Bt)(n):(0,a.kl)(n)}document.addEventListener("DOMContentLoaded",(function(a){document.querySelectorAll(".usa-combo-box").forEach((function(a){var r=a.querySelector("input"),o=a.querySelector("select");if(r&&o){var i=o.getAttribute("data-default-value"),s=a.querySelector(".usa-combo-box__clear-input");if(s){var l=a.querySelector("#".concat(r.id,"--list")),c=new MutationObserver((function(n){n.forEach((function(n){"childList"===n.type&&function(n,a,r){if(a&&!a.querySelector('[data-value=""]')&&!t){var o=document.createElement("li");o.setAttribute("role","option"),o.setAttribute("data-value",""),o.classList.add("usa-combo-box__list-option"),r||o.classList.add("usa-combo-box__list-option--selected"),o.textContent="⎯",a.insertBefore(o,a.firstChild),o.addEventListener("click",(function(t){t.preventDefault(),t.stopPropagation(),e=!1,n.click(),e=!0}))}}(s,l,i)}))}));c.observe(l,{childList:!0,subtree:!0}),r.addEventListener("input",(function(){t=!0})),r.addEventListener("blur",(function(){t=!1})),n(o,i,s),o.addEventListener("change",(function(){n(o,i,s)})),s.addEventListener("click",(function(t){if(e&&i){t.preventDefault(),t.stopPropagation(),r.click();var n=document.querySelectorAll(".usa-combo-box__list-option");n&&n.forEach((function(e){e.getAttribute("data-value")===i&&e.click()}))}}))}else console.warn("No clear element found")}else console.warn("No combobox element found")}))}))}(),function(e){var t="portfolio_requesting_entity",n=document.getElementById("id_".concat(t,"-requesting_entity_is_suborganization__fieldset")),r=null==n?void 0:n.querySelectorAll('input[name="'.concat(t,'-requesting_entity_is_suborganization"]')),o=document.getElementById("id_".concat(t,"-sub_organization")),i=document.getElementById("suborganization-container"),s=document.getElementById("suborganization-container__details");if(r&&o&&i&&s){var l="True"===(null===(e=Array.from(r).find((function(e){return e.checked})))||void 0===e?void 0:e.value),c=document.getElementById("id_".concat(t,"-is_requesting_new_suborganization"));o&&!Array.from(o.options).some((function(e){return"other"===e.value}))&&o.add(new Option("Other (enter your organization manually)","other")),"True"===c.value&&(o.value="other"),u(),r.forEach((function(e){e.addEventListener("click",(function(){return u(e)}))})),o.addEventListener("change",(function(){return u()}))}function u(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;null!=e&&(l=(null==e?void 0:e.checked)&&"True"===e.value),l?(0,a.kl)(i):(0,a.Bt)(i),c.value=l&&"other"===o.value?"True":"False","True"===c.value?(0,a.kl)(s):(0,a.Bt)(s)}}()},178:(e,t,n)=>{function a(e){e.classList.add("display-none")}function r(e){e.classList.remove("display-none")}n.d(t,{Bt:()=>a,kl:()=>r})}},t={};function n(a){var r=t[a];if(void 0!==r)return r.exports;var o=t[a]={exports:{}};return e[a](o,o.exports,n),o.exports}n.d=(e,t)=>{for(var a in t)n.o(t,a)&&!n.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n(635),n(686),n(790),n(178)})(); \ No newline at end of file From 2b3ce1cdfbb16ef01f7302d37619b284302ce6e9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 18 Nov 2024 15:05:25 -0500 Subject: [PATCH 057/148] Move unique logic from _get_requested_domain into fake_dot_gov, handle cases of missing phone or title --- .github/workflows/load-fixtures.yaml | 50 +++++++++++++++++++++ src/registrar/fixtures/fixtures_requests.py | 13 +++--- src/registrar/fixtures/fixtures_users.py | 14 ++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/load-fixtures.yaml diff --git a/.github/workflows/load-fixtures.yaml b/.github/workflows/load-fixtures.yaml new file mode 100644 index 000000000..108a54564 --- /dev/null +++ b/.github/workflows/load-fixtures.yaml @@ -0,0 +1,50 @@ +# Manually load fixtures to an environment of choice. + +name: Load fixtures +run-name: Manually load fixtures to sandbox of choice + +on: + workflow_dispatch: + inputs: + environment: + description: Which environment should we load data for? + type: 'choice' + options: + - ab + - backup + - el + - cb + - dk + - es + - gd + - ko + - ky + - nl + - rb + - rh + - rjm + - meoward + - bob + - hotgov + - litterbox + - ms + - ad + - ag + +jobs: + load-fixtures: + runs-on: ubuntu-latest + env: + CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME + CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD + steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - name: Load fake data for ${{ github.event.inputs.environment }} + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ github.event.inputs.environment }} + cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata" + diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index e869cda44..9f736d0d1 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -104,7 +104,10 @@ class DomainRequestFixture: @classmethod def fake_dot_gov(cls): - return f"{fake.slug()}.gov" + while True: + fake_name = f"{fake.slug()}.gov" + if not Domain.objects.filter(name=fake_name).exists(): + return DraftDomain.objects.create(name=fake_name) @classmethod def fake_expiration_date(cls): @@ -192,12 +195,8 @@ class DomainRequestFixture: if "requested_domain" in request_dict and request_dict["requested_domain"] is not None: return DraftDomain.objects.get_or_create(name=request_dict["requested_domain"])[0] - # Generate a unique fake domain - # This will help us avoid domain already approved log warnings - while True: - fake_name = cls.fake_dot_gov() - if not Domain.objects.filter(name=fake_name).exists(): - return DraftDomain.objects.create(name=fake_name) + # Generate a unique fake domain + return cls.fake_dot_gov() return request.requested_domain @classmethod diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index 343686028..0c4da9bac 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -327,8 +327,22 @@ class UserFixture: # Update `is_staff` for existing users if necessary users_to_update = [] for user in created_or_existing_users: + updated = False + + if not user.title: + user.title = "Peon" + updated = True + + if not user.phone: + user.phone = "2022222222" + updated = True + if not user.is_staff: user.is_staff = True + updated = True + + # Only append the user if any of the fields were updated + if updated: users_to_update.append(user) # Save any users that were updated From e90f1f9a5e1fbd81f8544cbea9e2dea210c4be34 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Mon, 18 Nov 2024 14:18:57 -0600 Subject: [PATCH 058/148] Update transfer_user.html --- src/registrar/templates/admin/transfer_user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/transfer_user.html b/src/registrar/templates/admin/transfer_user.html index 408000171..3ba136b93 100644 --- a/src/registrar/templates/admin/transfer_user.html +++ b/src/registrar/templates/admin/transfer_user.html @@ -16,7 +16,7 @@ - + From 1c0f99eedc2f39502ecdde28df6dc011cc8af1c6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:01:00 -0700 Subject: [PATCH 059/148] Use audit log instead --- src/registrar/admin.py | 9 --- ...te_user_portfolio_permission_invitation.py | 34 --------- ...0138_userportfoliopermission_invitation.py | 25 ------- src/registrar/models/portfolio_invitation.py | 4 +- .../models/user_portfolio_permission.py | 10 --- src/registrar/utility/model_annotations.py | 70 +++++++++++++++---- 6 files changed, 58 insertions(+), 94 deletions(-) delete mode 100644 src/registrar/management/commands/populate_user_portfolio_permission_invitation.py delete mode 100644 src/registrar/migrations/0138_userportfoliopermission_invitation.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 1679eeed2..0cab01d31 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1268,15 +1268,6 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin): _meta = Meta() - # Question for reviewers: should this include the invitation field? - # This is the same layout as before. - fieldsets = ( - ( - None, - {"fields": ("user", "portfolio", "invitation", "roles", "additional_permissions")}, - ), - ) - # Columns list_display = [ "user", diff --git a/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py b/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py deleted file mode 100644 index 7a73f7710..000000000 --- a/src/registrar/management/commands/populate_user_portfolio_permission_invitation.py +++ /dev/null @@ -1,34 +0,0 @@ -import logging -from django.core.management import BaseCommand -from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors, TerminalHelper -from registrar.models import UserPortfolioPermission, PortfolioInvitation -from auditlog.models import LogEntry - -logger = logging.getLogger(__name__) - - -class Command(BaseCommand, PopulateScriptTemplate): - help = "Loops through each UserPortfolioPermission object and populates the invitation field" - - def handle(self, **kwargs): - """Loops through each DomainRequest object and populates - its last_status_update and first_submitted_date values""" - self.existing_invitations = PortfolioInvitation.objects.filter( - portfolio__isnull=False, email__isnull=False - ).select_related("portfolio") - filter_condition = {"invitation__isnull": True, "portfolio__isnull": False, "user__email__isnull": False} - self.mass_update_records(UserPortfolioPermission, filter_condition, fields_to_update=["invitation"]) - - def update_record(self, record: UserPortfolioPermission): - """Associate the invitation to the right object""" - record.invitation = self.existing_invitations.filter( - email=record.user.email, portfolio=record.portfolio - ).first() - TerminalHelper.colorful_logger("INFO", "OKCYAN", f"{TerminalColors.OKCYAN}Adding invitation to {record}") - - def should_skip_record(self, record) -> bool: - """There is nothing to add if no invitation exists""" - return ( - not record - or not self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).exists() - ) diff --git a/src/registrar/migrations/0138_userportfoliopermission_invitation.py b/src/registrar/migrations/0138_userportfoliopermission_invitation.py deleted file mode 100644 index abac42ae2..000000000 --- a/src/registrar/migrations/0138_userportfoliopermission_invitation.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.10 on 2024-11-14 21:05 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0137_suborganization_city_suborganization_state_territory"), - ] - - operations = [ - migrations.AddField( - model_name="userportfoliopermission", - name="invitation", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="created_user_portfolio_permission", - to="registrar.portfolioinvitation", - ), - ), - ] diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index 0ea410211..61a6b7397 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -9,8 +9,6 @@ from registrar.models.user_portfolio_permission import UserPortfolioPermission from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField -from django.contrib.admin.models import LogEntry, ADDITION -from django.contrib.contenttypes.models import ContentType logger = logging.getLogger(__name__) @@ -103,7 +101,7 @@ class PortfolioInvitation(TimeStampedModel): # and create a role for that user on this portfolio user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( - portfolio=self.portfolio, user=user, invitation=self + portfolio=self.portfolio, user=user ) if self.roles and len(self.roles) > 0: user_portfolio_permission.roles = self.roles diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 424f09a17..cd48b1b71 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -65,16 +65,6 @@ class UserPortfolioPermission(TimeStampedModel): help_text="Select one or more additional permissions.", ) - # TODO - this needs a small script to update existing values - invitation = models.ForeignKey( - "registrar.PortfolioInvitation", - null=True, - blank=True, - # We don't want to accidentally delete invitations - on_delete=models.PROTECT, - related_name="created_user_portfolio_permission", - ) - def __str__(self): readable_roles = [] if self.roles: diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 105296855..bd1ad2be0 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -243,6 +243,22 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): # Get all members on this portfolio return Q(portfolio=portfolio) + @classmethod + def get_portfolio_invitation_id_query(cls): + """Gets the id of the portfolio invitation that created this UserPortfolioPermission. + This makes the assumption that if an invitation is retrieved, it must have created the given + UserPortfolioPermission object.""" + return Cast( + Subquery( + PortfolioInvitation.objects.filter( + status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED, + email=OuterRef(OuterRef("user__email")), + portfolio=OuterRef(OuterRef("portfolio")) + ).values("id")[:1] + ), + output_field=TextField() + ) + @classmethod def get_annotated_fields(cls, portfolio, csv_report=False): """ @@ -302,13 +318,11 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): & Q(user__permissions__domain__domain_info__portfolio=portfolio), ), "source": Value("permission", output_field=CharField()), - "invitation_date": Coalesce( - Func(F("invitation__created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()), - Value("Invalid date"), - output_field=TextField(), + "invitation_date": PortfolioInvitationModelAnnotation.get_invitation_date_query( + object_id_query=cls.get_portfolio_invitation_id_query() ), - "invited_by": PortfolioInvitationModelAnnotation.get_invited_by_from_audit_log_query( - object_id_query=Cast(OuterRef("invitation__id"), output_field=TextField()) + "invited_by": PortfolioInvitationModelAnnotation.get_invited_by_query( + object_id_query=cls.get_portfolio_invitation_id_query() ), } @@ -350,7 +364,39 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): return Q(portfolio=portfolio) @classmethod - def get_invited_by_from_audit_log_query(cls, object_id_query): + def get_invitation_date_query(cls, object_id_query): + """Returns the date at which the given invitation was created. + Grabs this data from the audit log, given that a portfolio invitation object + is specified via object_id_query.""" + return Coalesce( + Subquery( + LogEntry.objects.filter( + content_type=ContentType.objects.get_for_model(PortfolioInvitation), + object_id=object_id_query, + action_flag=ADDITION, + ) + .annotate( + # Action time will always be equivalent to created_at in this context + display_date=Func( + F("action_time"), + Value("YYYY-MM-DD"), + function="to_char", + output_field=TextField() + ) + ) + .order_by("action_time") + .values("display_date")[:1] + ), + Value("Invalid date"), + output_field=TextField() + ) + + + @classmethod + def get_invited_by_query(cls, object_id_query): + """Returns the user that created the given portfolio invitation. + Grabs this data from the audit log, given that a portfolio invitation object + is specified via object_id_query.""" return Coalesce( Subquery( LogEntry.objects.filter( @@ -374,7 +420,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): ) ) .order_by("action_time") - .values("display_email") + .values("display_email")[:1] ), Value("Unknown"), output_field=CharField(), @@ -415,15 +461,13 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): ) ), "source": Value("invitation", output_field=CharField()), - "invitation_date": Coalesce( - Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()), - Value("Invalid date"), - output_field=TextField(), + "invitation_date": cls.get_invitation_date_query( + object_id_query=Cast(OuterRef("id"), output_field=TextField()) ), # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. # Grab the invitation creator from the audit log. This will need to be replaced with a creator field. # When that happens, just replace this with F("invitation__creator") - "invited_by": cls.get_invited_by_from_audit_log_query( + "invited_by": cls.get_invited_by_query( object_id_query=Cast(OuterRef("id"), output_field=TextField()) ), } From 5f5bc4f616c95d2f739790614f53ed5de24961a9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:17:19 -0700 Subject: [PATCH 060/148] lint Just need unit tests! --- src/registrar/models/utility/orm_helper.py | 2 +- .../templates/includes/members_table.html | 2 +- src/registrar/utility/csv_export.py | 12 ++------ src/registrar/utility/model_annotations.py | 30 +++++++++---------- src/registrar/views/portfolios.py | 8 ++--- 5 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/registrar/models/utility/orm_helper.py b/src/registrar/models/utility/orm_helper.py index 4f4665216..63ff41d28 100644 --- a/src/registrar/models/utility/orm_helper.py +++ b/src/registrar/models/utility/orm_helper.py @@ -1,7 +1,7 @@ from django.db.models.expressions import Func -class ArrayRemove(Func): +class ArrayRemoveNull(Func): """Custom Func to use array_remove to remove null values""" function = "array_remove" diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index 09cebda2e..c0c6d803c 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -8,7 +8,7 @@
      - diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 664a038f2..c1a11d85c 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -812,7 +812,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): @override_flag("organization_members", active=True) @less_console_noise_decorator def test_member_export(self): - """Tests the member export report""" + """Tests the member export report by comparing the csv output.""" content_type = ContentType.objects.get_for_model(PortfolioInvitation) LogEntry.objects.create( user=self.lebowski_user, diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 537d61538..ab806f81a 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -175,7 +175,7 @@ class MemberExport(BaseExport): "additional_permissions_display", "member_display", "domain_info", - "source", + "type", "invitation_date", "invited_by", ] diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 6fff51c2c..453653b52 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -280,10 +280,12 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): F("user__last_login"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField() ) else: + # an array of domains, with id and name, colon separated domain_query = Concat( F("user__permissions__domain_id"), Value(":"), F("user__permissions__domain__name"), + # specify the output_field to ensure union has same column types output_field=CharField(), ) last_active_query = Cast(F("user__last_login"), output_field=TextField()) @@ -299,7 +301,9 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): ), "additional_permissions_display": F("additional_permissions"), "member_display": Case( + # If email is present and not blank, use email When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), + # If first name or last name is present, use concatenation of first_name + " " + last_name When( Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), then=Concat( @@ -308,16 +312,18 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): Coalesce(F("user__last_name"), Value("")), ), ), + # If neither, use an empty string default=Value(""), output_field=CharField(), ), "domain_info": ArrayAgg( domain_query, distinct=True, + # only include domains in portfolio filter=Q(user__permissions__domain__isnull=False) & Q(user__permissions__domain__domain_info__portfolio=portfolio), ), - "source": Value("permission", output_field=CharField()), + "type": Value("member", output_field=CharField()), "invitation_date": PortfolioInvitationModelAnnotation.get_invitation_date_query( object_id_query=cls.get_portfolio_invitation_id_query() ), @@ -452,13 +458,14 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): "last_active": Value("Invited", output_field=TextField()), "additional_permissions_display": F("additional_permissions"), "member_display": F("email"), + # Use ArrayRemove to return an empty list when no domain invitations are found "domain_info": ArrayRemoveNull( ArrayAgg( Subquery(domain_invitations.values("domain_info")), distinct=True, ) ), - "source": Value("invitation", output_field=CharField()), + "type": Value("invitedmember", output_field=CharField()), "invitation_date": cls.get_invitation_date_query( object_id_query=Cast(OuterRef("id"), output_field=TextField()) ), diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 3bf761858..bf89dcd82 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -66,7 +66,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): "additional_permissions_display", "member_display", "domain_info", - "source", + "type", ) def initial_invitations_search(self, portfolio): @@ -83,7 +83,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): "additional_permissions_display", "member_display", "domain_info", - "source", + "type", ) def apply_search_term(self, queryset, request): @@ -119,12 +119,12 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or []) - action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]}) + action_url = reverse(item["type"], kwargs={"pk": item["id"]}) # Serialize member data member_json = { - "id": item.get("id", ""), - "source": item.get("source", ""), + "id": item.get("id", ""), # id is id of UserPortfolioPermission or PortfolioInvitation + "type": item.get("type", ""), # source is member or invitedmember "name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])), "email": item.get("email_display", ""), "member_display": item.get("member_display", ""), diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index d35ae20dd..373d1619d 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -461,14 +461,7 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View): def get(self, request): """Add additional context data to the template.""" - # Get portfolio from session - portfolio = request.session.get("portfolio") - context = {} - if portfolio: - user_count = portfolio.portfolio_users.count() - invitation_count = PortfolioInvitation.objects.filter(portfolio=portfolio).count() - context.update({"member_count": user_count + invitation_count}) - return render(request, "portfolio_members.html", context=context) + return render(request, "portfolio_members.html") class NewMemberView(PortfolioMembersPermissionView, FormMixin): diff --git a/src/registrar/views/report_views.py b/src/registrar/views/report_views.py index 880de5d79..cff177d6d 100644 --- a/src/registrar/views/report_views.py +++ b/src/registrar/views/report_views.py @@ -175,9 +175,10 @@ class ExportMembersPortfolio(View): def get(self, request, *args, **kwargs): """Returns the members report""" - portfolio_display = "portfolio" + # Swap the spaces for dashes to make the formatted name look prettier + portfolio_display = "organization" if request.session.get("portfolio"): - portfolio_display = str(request.session.get("portfolio")).replace(" ", "-") + portfolio_display = str(request.session.get("portfolio")).lower().replace(" ", "-") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="members-for-{portfolio_display}.csv"' From 01e6b136625bcb8edb9a9783c9747940cb3d7970 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:39:10 -0700 Subject: [PATCH 083/148] Add comment + cleanup --- src/registrar/assets/js/get-gov.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index f204092be..82ca36735 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2789,6 +2789,8 @@ document.addEventListener('DOMContentLoaded', function() { const selectParent = select?.parentElement; const suborgContainer = document.getElementById("suborganization-container"); const suborgDetailsContainer = document.getElementById("suborganization-container__details"); + // Make sure all crucial page elements exist before proceeding. + // This more or less ensures that we are on the Requesting Entity page, and not elsewhere. if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return; // requestingSuborganization: This just broadly determines if they're requesting a suborg at all @@ -2800,7 +2802,6 @@ document.addEventListener('DOMContentLoaded', function() { if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True"; requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer); requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False"; - if (requestingNewSuborganization.value === "True") { selectParent.classList.add("padding-bottom-2"); showElement(suborgDetailsContainer); @@ -2808,7 +2809,6 @@ document.addEventListener('DOMContentLoaded', function() { selectParent.classList.remove("padding-bottom-2"); hideElement(suborgDetailsContainer); } - requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer); } // Add fake "other" option to sub_organization select From 9e35575001e459546a9e70e23e09b1253041370e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:45:35 -0700 Subject: [PATCH 084/148] Cleanup hide_requests --- src/registrar/views/domain_request.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 4dbfd3e70..da194755f 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -317,15 +317,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): # Clear context so the prop getter won't create a request here. # Creating a request will be handled in the post method for the # intro page. - return render( - request, - "domain_request_intro.html", - { - "hide_requests": False, - "hide_domains": False, - "hide_members": False, - }, - ) + return render(request, "domain_request_intro.html") else: return self.goto(self.steps.first) @@ -487,12 +479,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "user": self.request.user, "requested_domain__name": requested_domain_name, } - - # Hides the requests and domains buttons in the navbar - context["hide_requests"] = self.is_portfolio - context["hide_domains"] = self.is_portfolio context["domain_request_id"] = self.domain_request.id - return context def get_step_list(self) -> list: From 6d8a4ea93a390271ad16df3a80b9fd66354e8d9e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:49:31 -0700 Subject: [PATCH 085/148] Remove checks on hide_requests / etc --- src/registrar/templates/includes/header_extended.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/includes/header_extended.html b/src/registrar/templates/includes/header_extended.html index 6384b5249..831b8b958 100644 --- a/src/registrar/templates/includes/header_extended.html +++ b/src/registrar/templates/includes/header_extended.html @@ -34,7 +34,6 @@
        - {% if not hide_domains %}
      • {% if has_any_domains_portfolio_permission %} {% url 'domains' as url %} @@ -45,14 +44,13 @@ Domains
      • - {% endif %} - {% if has_organization_requests_flag and not hide_requests %} + {% if has_organization_requests_flag %}
      • {% if has_edit_request_portfolio_permission %} @@ -93,7 +91,7 @@
      • {% endif %} - {% if has_organization_members_flag and not hide_members %} + {% if has_organization_members_flag %}
      • Members From 4f67c18642375275275825fe4b43ba08cb0e58c7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:50:46 -0700 Subject: [PATCH 086/148] Fix tests --- src/registrar/tests/test_views_request.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index a73fac5a8..32cdda06a 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -3205,11 +3205,6 @@ class TestDomainRequestWizard(TestWithUser, WebTest): expected_url = reverse("domain-request:portfolio_requesting_entity", kwargs={"id": domain_request.id}) # This returns the entire url, thus "in" self.assertIn(expected_url, detail_page.request.url) - - # We shouldn't show the "domains" and "domain requests" buttons - # on this page. - self.assertNotContains(detail_page, "Domains") - self.assertNotContains(detail_page, "Domain requests") else: self.fail(f"Expected a redirect, but got a different response: {response}") From b791daf10cbeeaf82c19e6caa036e00fcc7897db Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:04:24 -0700 Subject: [PATCH 087/148] Guard report behind perms check Not strictly necessary as we check anyway, but double security --- src/registrar/views/report_views.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/report_views.py b/src/registrar/views/report_views.py index cff177d6d..56867216e 100644 --- a/src/registrar/views/report_views.py +++ b/src/registrar/views/report_views.py @@ -174,11 +174,23 @@ class ExportMembersPortfolio(View): def get(self, request, *args, **kwargs): """Returns the members report""" + portfolio = request.session.get("portfolio") + + # Check if the user has organization access + if not request.user.is_org_user(request): + return render(request, "403.html", status=403) + + # Check if the user has member permissions + if ( + not request.user.has_view_members_portfolio_permission(portfolio) + and not request.user.has_edit_members_portfolio_permission(portfolio) + ): + return render(request, "403.html", status=403) # Swap the spaces for dashes to make the formatted name look prettier portfolio_display = "organization" - if request.session.get("portfolio"): - portfolio_display = str(request.session.get("portfolio")).lower().replace(" ", "-") + if portfolio: + portfolio_display = str(portfolio).lower().replace(" ", "-") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="members-for-{portfolio_display}.csv"' From 4ab526ceb5629c2bfc20e0366d4c2b19e0567669 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:50:27 -0700 Subject: [PATCH 088/148] Lint! --- src/registrar/views/report_views.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/registrar/views/report_views.py b/src/registrar/views/report_views.py index 56867216e..1b1798d69 100644 --- a/src/registrar/views/report_views.py +++ b/src/registrar/views/report_views.py @@ -179,12 +179,11 @@ class ExportMembersPortfolio(View): # Check if the user has organization access if not request.user.is_org_user(request): return render(request, "403.html", status=403) - + # Check if the user has member permissions - if ( - not request.user.has_view_members_portfolio_permission(portfolio) - and not request.user.has_edit_members_portfolio_permission(portfolio) - ): + if not request.user.has_view_members_portfolio_permission( + portfolio + ) and not request.user.has_edit_members_portfolio_permission(portfolio): return render(request, "403.html", status=403) # Swap the spaces for dashes to make the formatted name look prettier From ec8e8b1f65093d90d018744cf175bfc56320cbc8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 13:15:27 -0500 Subject: [PATCH 089/148] lint --- src/registrar/fixtures/fixtures_requests.py | 10 +++++----- src/registrar/fixtures/fixtures_users.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index 9f736d0d1..c413f4b62 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -194,8 +194,8 @@ class DomainRequestFixture: if not request.requested_domain: if "requested_domain" in request_dict and request_dict["requested_domain"] is not None: return DraftDomain.objects.get_or_create(name=request_dict["requested_domain"])[0] - - # Generate a unique fake domain + + # Generate a unique fake domain return cls.fake_dot_gov() return request.requested_domain @@ -233,19 +233,19 @@ class DomainRequestFixture: except Exception as e: logger.warning(f"Expected fixture portfolio, did not find it: {e}") return None - + @classmethod def _get_random_sub_organization(cls, request): try: # Filter Suborganizations by the request's portfolio portfolio_suborganizations = Suborganization.objects.filter(portfolio=request.portfolio) - + # Assuming SuborganizationFixture.SUBORGS is a list of dictionaries with a "name" key suborganization_names = [suborg["name"] for suborg in SuborganizationFixture.SUBORGS] # Further filter by names in suborganization_names suborganization_options = portfolio_suborganizations.filter(name__in=suborganization_names) - + # Randomly choose one if any exist return random.choice(suborganization_options) if suborganization_options.exists() else None # nosec except Exception as e: diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index 0c4da9bac..b3ab530c3 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -161,7 +161,6 @@ class UserFixture: ] STAFF = [ - # { # "username": "994b7a90-f1d1-4140-a3d2-ff34183c7ee2", # "first_name": "Rach test staff", @@ -344,7 +343,7 @@ class UserFixture: # Only append the user if any of the fields were updated if updated: users_to_update.append(user) - + # Save any users that were updated if users_to_update: User.objects.bulk_update(users_to_update, ["is_staff"]) From 0958eda7cc454b124997e27695a70692c9573bc2 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 13:31:13 -0500 Subject: [PATCH 090/148] refactor for readability --- src/registrar/fixtures/fixtures_users.py | 148 +++++++++++++---------- 1 file changed, 81 insertions(+), 67 deletions(-) diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index b3ab530c3..be4afa311 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -281,80 +281,24 @@ class UserFixture: """Loads the users into the database and assigns them to the specified group.""" logger.info(f"Going to load {len(users)} users for group {group_name}") + # Step 1: Fetch the group group = UserGroup.objects.get(name=group_name) - # Prepare sets of existing usernames and IDs in one query - user_identifiers = [(user.get("username"), user.get("id")) for user in users] - existing_users = User.objects.filter( - username__in=[user[0] for user in user_identifiers] + [user[1] for user in user_identifiers] - ).values_list("username", "id") + # Step 2: Identify new and existing users + existing_usernames, existing_user_ids = cls._get_existing_users(users) + new_users = cls._prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers) - existing_usernames = set(user[0] for user in existing_users) - existing_user_ids = set(user[1] for user in existing_users) - - # Filter out users with existing IDs or usernames - new_users = [ - User( - id=user_data.get("id"), - first_name=user_data.get("first_name"), - last_name=user_data.get("last_name"), - username=user_data.get("username"), - email=user_data.get("email", ""), - title=user_data.get("title", "Peon"), - phone=user_data.get("phone", "2022222222"), - is_active=user_data.get("is_active", True), - is_staff=True, - is_superuser=are_superusers, - ) - for user_data in users - if user_data.get("username") not in existing_usernames and user_data.get("id") not in existing_user_ids - ] - - # Perform bulk creation for new users - if new_users: - try: - User.objects.bulk_create(new_users) - logger.info(f"Created {len(new_users)} new users.") - except Exception as e: - logger.error(f"Unexpected error during user bulk creation: {e}") - else: - logger.info("No new users to create.") + # Step 3: Create new users + cls._create_new_users(new_users) + # Step 4: Update existing users # Get all users to be updated (both new and existing) created_or_existing_users = User.objects.filter(username__in=[user.get("username") for user in users]) + users_to_update = cls._get_users_to_update(created_or_existing_users) + cls._update_existing_users(users_to_update) - # Update `is_staff` for existing users if necessary - users_to_update = [] - for user in created_or_existing_users: - updated = False - - if not user.title: - user.title = "Peon" - updated = True - - if not user.phone: - user.phone = "2022222222" - updated = True - - if not user.is_staff: - user.is_staff = True - updated = True - - # Only append the user if any of the fields were updated - if updated: - users_to_update.append(user) - - # Save any users that were updated - if users_to_update: - User.objects.bulk_update(users_to_update, ["is_staff"]) - logger.info(f"Updated {len(users_to_update)} existing users to have is_staff=True.") - - # Filter out users who are already in the group - users_not_in_group = created_or_existing_users.exclude(groups__id=group.id) - - # Add only users who are not already in the group - if users_not_in_group.exists(): - group.user_set.add(*users_not_in_group) + # Step 5: Assign users to the group + cls._assign_users_to_group(group, created_or_existing_users) logger.info(f"Users loaded for group {group_name}.") @@ -386,6 +330,76 @@ class UserFixture: else: logger.info("No allowed emails to load") + @staticmethod + def _get_existing_users(users): + user_identifiers = [(user.get("username"), user.get("id")) for user in users] + existing_users = User.objects.filter( + username__in=[user[0] for user in user_identifiers] + [user[1] for user in user_identifiers] + ).values_list("username", "id") + existing_usernames = set(user[0] for user in existing_users) + existing_user_ids = set(user[1] for user in existing_users) + return existing_usernames, existing_user_ids + + @staticmethod + def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers): + return [ + User( + id=user_data.get("id"), + first_name=user_data.get("first_name"), + last_name=user_data.get("last_name"), + username=user_data.get("username"), + email=user_data.get("email", ""), + title=user_data.get("title", "Peon"), + phone=user_data.get("phone", "2022222222"), + is_active=user_data.get("is_active", True), + is_staff=True, + is_superuser=are_superusers, + ) + for user_data in users + if user_data.get("username") not in existing_usernames and user_data.get("id") not in existing_user_ids + ] + + @staticmethod + def _create_new_users(new_users): + if new_users: + try: + User.objects.bulk_create(new_users) + logger.info(f"Created {len(new_users)} new users.") + except Exception as e: + logger.error(f"Unexpected error during user bulk creation: {e}") + else: + logger.info("No new users to create.") + + @staticmethod + def _get_users_to_update(users): + users_to_update = [] + for user in users: + updated = False + if not user.title: + user.title = "Peon" + updated = True + if not user.phone: + user.phone = "2022222222" + updated = True + if not user.is_staff: + user.is_staff = True + updated = True + if updated: + users_to_update.append(user) + return users_to_update + + @staticmethod + def _update_existing_users(users_to_update): + if users_to_update: + User.objects.bulk_update(users_to_update, ["is_staff", "title", "phone"]) + logger.info(f"Updated {len(users_to_update)} existing users.") + + @staticmethod + def _assign_users_to_group(group, users): + users_not_in_group = users.exclude(groups__id=group.id) + if users_not_in_group.exists(): + group.user_set.add(*users_not_in_group) + @classmethod def load(cls): with transaction.atomic(): From 981cdb31b69458fb505897885a9391ae339f277c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:29:53 -0700 Subject: [PATCH 091/148] Replace invitation date => joined date --- src/registrar/tests/test_reports.py | 42 +++++++++++++--------- src/registrar/utility/csv_export.py | 6 ++-- src/registrar/utility/model_annotations.py | 38 ++------------------ 3 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index c1a11d85c..82942603a 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -805,6 +805,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): class MemberExportTest(MockDbForIndividualTests, MockEppLib): def setUp(self): + """Override of the base setUp to add a request factory""" super().setUp() self.factory = RequestFactory() @@ -813,6 +814,12 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): @less_console_noise_decorator def test_member_export(self): """Tests the member export report by comparing the csv output.""" + # == Data setup == # + # Set last_login for some users + active_date = timezone.make_aware(datetime(2024, 2, 1)) + User.objects.filter(id__in=[self.custom_superuser.id, self.custom_staffuser.id]).update(last_login=active_date) + + # Create a logentry for meoward, created by lebowski to test invited_by. content_type = ContentType.objects.get_for_model(PortfolioInvitation) LogEntry.objects.create( user=self.lebowski_user, @@ -824,8 +831,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): action_time=timezone.make_aware(datetime(2023, 4, 12)), ) - # Create log entries for each remaining invitation. - # Exclude meoward and tired_user (to test null dates, etc). + # Create log entries for each remaining invitation. Exclude meoward and tired_user. for invitation in PortfolioInvitation.objects.exclude( id__in=[self.portfolio_invitation_1.id, self.portfolio_invitation_3.id] ): @@ -838,9 +844,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): change_message="Created invitation", action_time=timezone.make_aware(datetime(2024, 1, 15)), ) - # Set last_login for some users - active_date = timezone.make_aware(datetime(2024, 2, 1)) - User.objects.filter(id__in=[self.custom_superuser.id, self.custom_staffuser.id]).update(last_login=active_date) + # Retrieve invitations with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): self.meoward_user.check_portfolio_invitations_on_login() @@ -849,6 +853,12 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): self.custom_superuser.check_portfolio_invitations_on_login() self.custom_staffuser.check_portfolio_invitations_on_login() + # Update the created at date on UserPortfolioPermission, so we can test a consistent date. + UserPortfolioPermission.objects.filter(portfolio=self.portfolio_1).update( + created_at=timezone.make_aware(datetime(2022, 4, 1)) + ) + # == End of data setup == # + # Create a request and add the user to the request request = self.factory.get("/") request.user = self.user @@ -867,20 +877,20 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): csv_content = csv_file.read() expected_content = ( # Header - "Email,Organization admin,Invited by,Invitation date,Last active,Domain requests," + "Email,Organization admin,Invited by,Joined date,Last active,Domain requests," "Member management,Domain management,Number of domains,Domains\n" # Content - "meoward@rocks.com,False,big_lebowski@dude.co,2023-04-12,Invalid date,None," + "meoward@rocks.com,False,big_lebowski@dude.co,2022-04-01,Invalid date,None," 'Manager,True,2,"adomain2.gov,cdomain1.gov"\n' - "big_lebowski@dude.co,False,help@get.gov,2024-01-15,Invalid date,None,Viewer,True,1,cdomain1.gov\n" - "tired_sleepy@igorville.gov,False,System,Unknown,Invalid date,Viewer,None,False,0,\n" - "icy_superuser@igorville.gov,True,help@get.gov,2024-01-15,2024-02-01,Viewer Requester,Manager,False,0,\n" - "cozy_staffuser@igorville.gov,True,help@get.gov,2024-01-15,2024-02-01,Viewer Requester,None,False,0,\n" - "nonexistentmember_1@igorville.gov,False,help@get.gov,2024-01-15,Invited,None,Manager,False,0,\n" - "nonexistentmember_2@igorville.gov,False,help@get.gov,2024-01-15,Invited,None,Viewer,False,0,\n" - "nonexistentmember_3@igorville.gov,False,help@get.gov,2024-01-15,Invited,Viewer,None,False,0,\n" - "nonexistentmember_4@igorville.gov,True,help@get.gov,2024-01-15,Invited,Viewer Requester,Manager,False,0,\n" - "nonexistentmember_5@igorville.gov,True,help@get.gov,2024-01-15,Invited,Viewer Requester,None,False,0,\n" + "big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,Viewer,True,1,cdomain1.gov\n" + "tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,None,False,0,\n" + "icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,Manager,False,0,\n" + "cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,None,False,0,\n" + "nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Manager,False,0,\n" + "nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Viewer,False,0,\n" + "nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,Viewer,None,False,0,\n" + "nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer Requester,Manager,False,0,\n" + "nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer Requester,None,False,0,\n" ) # Normalize line endings and remove commas, # spaces and leading/trailing whitespace diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index ab806f81a..bd666923c 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -176,7 +176,7 @@ class MemberExport(BaseExport): "member_display", "domain_info", "type", - "invitation_date", + "joined_date", "invited_by", ] permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values( @@ -197,7 +197,7 @@ class MemberExport(BaseExport): "Email", "Organization admin", "Invited by", - "Invitation date", + "Joined date", "Last active", "Domain requests", "Member management", @@ -226,7 +226,7 @@ class MemberExport(BaseExport): "Email": model.get("email_display"), "Organization admin": is_admin, "Invited by": model.get("invited_by"), - "Invitation date": model.get("invitation_date"), + "Joined date": model.get("joined_date"), "Last active": model.get("last_active"), "Domain requests": domain_request_display, "Member management": member_perm_display, diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 453653b52..f0ef70396 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -324,9 +324,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): & Q(user__permissions__domain__domain_info__portfolio=portfolio), ), "type": Value("member", output_field=CharField()), - "invitation_date": PortfolioInvitationModelAnnotation.get_invitation_date_query( - object_id_query=cls.get_portfolio_invitation_id_query() - ), + "joined_date": Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()), "invited_by": PortfolioInvitationModelAnnotation.get_invited_by_query( object_id_query=cls.get_portfolio_invitation_id_query() ), @@ -369,33 +367,6 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): # Get all members on this portfolio return Q(portfolio=portfolio) - @classmethod - def get_invitation_date_query(cls, object_id_query): - """Returns the date at which the given invitation was created. - Grabs this data from the audit log, given that a portfolio invitation object - is specified via object_id_query.""" - return Coalesce( - Subquery( - LogEntry.objects.filter( - content_type=ContentType.objects.get_for_model(PortfolioInvitation), - object_id=object_id_query, - action_flag=ADDITION, - ) - .annotate( - # Action time will always be equivalent to created_at in this context. - # Using this instead of created_at is a lot simpler and more performant, - # as otherwise a Case and Subquery need to be used. - display_date=Func( - F("action_time"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField() - ) - ) - .order_by("action_time") - .values("display_date")[:1] - ), - Value("Unknown"), - output_field=TextField(), - ) - @classmethod def get_invited_by_query(cls, object_id_query): """Returns the user that created the given portfolio invitation. @@ -466,12 +437,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): ) ), "type": Value("invitedmember", output_field=CharField()), - "invitation_date": cls.get_invitation_date_query( - object_id_query=Cast(OuterRef("id"), output_field=TextField()) - ), - # TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket. - # Grab the invitation creator from the audit log. This will need to be replaced with a creator field. - # When that happens, just replace this with F("invitation__creator") + "joined_date": Value("Unretrieved", output_field=CharField()), "invited_by": cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=TextField())), } From 3f8db08fb82936a21360292d611c3028e7568d83 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:49:58 -0700 Subject: [PATCH 092/148] Code cleanup --- src/registrar/tests/test_reports.py | 3 +- src/registrar/utility/csv_export.py | 32 +++++++++------------- src/registrar/utility/model_annotations.py | 10 +++---- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 82942603a..1289c2467 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -889,7 +889,8 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): "nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Manager,False,0,\n" "nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Viewer,False,0,\n" "nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,Viewer,None,False,0,\n" - "nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer Requester,Manager,False,0,\n" + "nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved," + "Invited,Viewer Requester,Manager,False,0,\n" "nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer Requester,None,False,0,\n" ) # Normalize line endings and remove commas, diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index bd666923c..db5eb1657 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -146,6 +146,8 @@ class BaseExport(BaseModelAnnotation): class MemberExport(BaseExport): + """CSV export for the MembersTable. The members table combines the content + of three tables: PortfolioInvitation, UserPortfolioPermission, and DomainInvitation.""" @classmethod def model(self): @@ -185,8 +187,7 @@ class MemberExport(BaseExport): invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values( *shared_columns ) - queryset_dict = convert_queryset_to_dict(permissions.union(invitations), is_model=False) - return queryset_dict + return convert_queryset_to_dict(permissions.union(invitations), is_model=False) @classmethod def get_columns(cls): @@ -213,30 +214,23 @@ class MemberExport(BaseExport): Given a set of columns and a model dictionary, generate a new row from cleaned column data. Must be implemented by subclasses """ - roles = model.get("roles") - additional_permissions = model.get("additional_permissions_display") - is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (roles or []) - domain_request_display = UserPortfolioPermission.get_domain_request_permission_display( - roles, additional_permissions - ) - member_perm_display = UserPortfolioPermission.get_member_permission_display(roles, additional_permissions) + roles = model.get("roles", []) + permissions = model.get("additional_permissions_display") user_managed_domains = model.get("domain_info", []) - managed_domains_as_csv = ",".join(user_managed_domains) + length_user_managed_domains = len(user_managed_domains) FIELDS = { "Email": model.get("email_display"), - "Organization admin": is_admin, + "Organization admin": bool(UserPortfolioRoleChoices.ORGANIZATION_ADMIN in roles), "Invited by": model.get("invited_by"), "Joined date": model.get("joined_date"), "Last active": model.get("last_active"), - "Domain requests": domain_request_display, - "Member management": member_perm_display, - "Domain management": len(user_managed_domains) > 0, - "Number of domains": len(user_managed_domains), - "Domains": managed_domains_as_csv, + "Domain requests": UserPortfolioPermission.get_domain_request_permission_display(roles, permissions), + "Member management": UserPortfolioPermission.get_member_permission_display(roles, permissions), + "Domain management": bool(length_user_managed_domains > 0), + "Number of domains": length_user_managed_domains, + "Domains": ",".join(user_managed_domains), } - - row = [FIELDS.get(column, "") for column in columns] - return row + return [FIELDS.get(column, "") for column in columns] class DomainExport(BaseExport): diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index f0ef70396..0fc430f4f 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -256,7 +256,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): portfolio=OuterRef(OuterRef("portfolio")), ).values("id")[:1] ), - output_field=TextField(), + output_field=CharField(), ) @classmethod @@ -297,7 +297,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): "last_active": Coalesce( last_active_query, Value("Invalid date"), - output_field=TextField(), + output_field=CharField(), ), "additional_permissions_display": F("additional_permissions"), "member_display": Case( @@ -324,7 +324,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): & Q(user__permissions__domain__domain_info__portfolio=portfolio), ), "type": Value("member", output_field=CharField()), - "joined_date": Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()), + "joined_date": Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=CharField()), "invited_by": PortfolioInvitationModelAnnotation.get_invited_by_query( object_id_query=cls.get_portfolio_invitation_id_query() ), @@ -426,7 +426,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): "first_name": Value(None, output_field=CharField()), "last_name": Value(None, output_field=CharField()), "email_display": F("email"), - "last_active": Value("Invited", output_field=TextField()), + "last_active": Value("Invited", output_field=CharField()), "additional_permissions_display": F("additional_permissions"), "member_display": F("email"), # Use ArrayRemove to return an empty list when no domain invitations are found @@ -438,7 +438,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): ), "type": Value("invitedmember", output_field=CharField()), "joined_date": Value("Unretrieved", output_field=CharField()), - "invited_by": cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=TextField())), + "invited_by": cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=CharField())), } @classmethod From 2a3f67b5cefa46da2e3263554077a07e3d4daa23 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 20 Nov 2024 16:21:11 -0500 Subject: [PATCH 093/148] updated --- src/registrar/assets/sass/_theme/_base.scss | 6 +----- src/registrar/templates/portfolio_requests.html | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 7d5a72a82..d8d7fd7ee 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -252,8 +252,4 @@ abbr[title] { .break-word { word-break: break-word; -} - -.maxwidth-386{ - max-width: 386px !important; -} +} \ No newline at end of file diff --git a/src/registrar/templates/portfolio_requests.html b/src/registrar/templates/portfolio_requests.html index 6bb4902bf..467141077 100644 --- a/src/registrar/templates/portfolio_requests.html +++ b/src/registrar/templates/portfolio_requests.html @@ -19,7 +19,7 @@ {% if has_edit_request_portfolio_permission %}
        -

        Domain requests can only be modified by the person who created the request.

        +

        Domain requests can only be modified by the person who created the request.

        From 1e16aeffb434818126781393e7d7aa199f33ee02 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 19:40:24 -0500 Subject: [PATCH 094/148] hand merge kebab js --- src/registrar/assets/modules/helpers-uswds.js | 10 +- src/registrar/assets/modules/helpers.js | 2 +- src/registrar/assets/modules/main.js | 3 + .../assets/modules/portfolio-member-page.js | 42 ++ src/registrar/assets/modules/table-base.js | 433 +++++++++++++--- .../assets/modules/table-domain-requests.js | 490 ++++++------------ src/registrar/assets/modules/table-domains.js | 192 +++---- .../assets/modules/table-member-domains.js | 121 +---- src/registrar/assets/modules/table-members.js | 401 ++++++++------ 9 files changed, 909 insertions(+), 785 deletions(-) create mode 100644 src/registrar/assets/modules/portfolio-member-page.js diff --git a/src/registrar/assets/modules/helpers-uswds.js b/src/registrar/assets/modules/helpers-uswds.js index e182d65c0..bb861ab9c 100644 --- a/src/registrar/assets/modules/helpers-uswds.js +++ b/src/registrar/assets/modules/helpers-uswds.js @@ -21,13 +21,13 @@ export function initializeTooltips() { * Initialize USWDS modals by calling on method. Requires that uswds-edited.js be loaded * before get-gov.js. uswds-edited.js adds the modal module to the window to be accessible * directly in get-gov.js. - * initializeModals adds modal-related DOM elements, based on other DOM elements existing in + * uswdsInitializeModals adds modal-related DOM elements, based on other DOM elements existing in * the page. It needs to be called only once for any particular DOM element; otherwise, it * will initialize improperly. Therefore, if DOM elements change dynamically and include - * DOM elements with modal classes, unloadModals needs to be called before initializeModals. + * DOM elements with modal classes, uswdsUnloadModals needs to be called before uswdsInitializeModals. * */ -export function initializeModals() { +export function uswdsInitializeModals() { window.modal.on(); } @@ -35,9 +35,9 @@ export function initializeModals() { * Unload existing USWDS modals by calling off method. Requires that uswds-edited.js be * loaded before get-gov.js. uswds-edited.js adds the modal module to the window to be * accessible directly in get-gov.js. - * See note above with regards to calling this method relative to initializeModals. + * See note above with regards to calling this method relative to uswdsInitializeModals. * */ -export function unloadModals() { +export function uswdsUnloadModals() { window.modal.off(); } diff --git a/src/registrar/assets/modules/helpers.js b/src/registrar/assets/modules/helpers.js index f02605269..ea8fe5661 100644 --- a/src/registrar/assets/modules/helpers.js +++ b/src/registrar/assets/modules/helpers.js @@ -7,7 +7,7 @@ export function showElement(element) { }; /** - * Helper function that scrolls to an element + * Helper function that scrolls to an element identified by a class or an id. * @param {string} attributeName - The string "class" or "id" * @param {string} attributeValue - The class or id name */ diff --git a/src/registrar/assets/modules/main.js b/src/registrar/assets/modules/main.js index f99ef188c..cd89540ab 100644 --- a/src/registrar/assets/modules/main.js +++ b/src/registrar/assets/modules/main.js @@ -9,6 +9,7 @@ import { initDomainsTable } from './table-domains.js'; import { initDomainRequestsTable } from './table-domain-requests.js'; import { initMembersTable } from './table-members.js'; import { initMemberDomainsTable } from './table-member-domains.js'; +import { initPortfolioMemberPageToggle } from './portfolio-member-page.js'; initDomainValidators(); @@ -40,3 +41,5 @@ initDomainsTable(); initDomainRequestsTable(); initMembersTable(); initMemberDomainsTable(); + +initPortfolioMemberPageToggle(); diff --git a/src/registrar/assets/modules/portfolio-member-page.js b/src/registrar/assets/modules/portfolio-member-page.js new file mode 100644 index 000000000..023836135 --- /dev/null +++ b/src/registrar/assets/modules/portfolio-member-page.js @@ -0,0 +1,42 @@ +import { uswdsInitializeModals } from './helpers-uswds.js'; +import { generateKebabHTML } from './table-base.js'; + +// This is specifically for the Member Profile (Manage Member) Page member/invitation removal +export function initPortfolioMemberPageToggle() { + document.addEventListener("DOMContentLoaded", () => { + const wrapperDeleteAction = document.getElementById("wrapper-delete-action") + if (wrapperDeleteAction) { + const member_type = wrapperDeleteAction.getAttribute("data-member-type"); + const member_id = wrapperDeleteAction.getAttribute("data-member-id"); + const num_domains = wrapperDeleteAction.getAttribute("data-num-domains"); + const member_name = wrapperDeleteAction.getAttribute("data-member-name"); + const member_email = wrapperDeleteAction.getAttribute("data-member-email"); + const member_delete_url = `${member_type}-${member_id}/delete`; + const unique_id = `${member_type}-${member_id}`; + + let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member"; + wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`); + + // This easter egg is only for fixtures that dont have names as we are displaying their emails + // All prod users will have emails linked to their account + MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction); + + uswdsInitializeModals(); + + // Now the DOM and modals are ready, add listeners to the submit buttons + const modals = document.querySelectorAll('.usa-modal__content'); + + modals.forEach(modal => { + const submitButton = modal.querySelector('.usa-modal__submit'); + const closeButton = modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + let delete_member_form = document.getElementById("member-delete-form"); + if (delete_member_form) { + delete_member_form.submit(); + } + }); + }); + } + }); +} diff --git a/src/registrar/assets/modules/table-base.js b/src/registrar/assets/modules/table-base.js index ed9cdc655..84cf64663 100644 --- a/src/registrar/assets/modules/table-base.js +++ b/src/registrar/assets/modules/table-base.js @@ -1,24 +1,149 @@ -import { hideElement, showElement, toggleCaret } from './helpers.js'; +import { hideElement, showElement, toggleCaret, scrollToElement } from './helpers.js'; -export class LoadTableBase { +/** +* Creates and adds a modal dialog to the DOM with customizable attributes and content. +* +* @param {string} id - A unique identifier for the modal, appended to the action for uniqueness. +* @param {string} ariaLabelledby - The ID of the element that labels the modal, for accessibility. +* @param {string} ariaDescribedby - The ID of the element that describes the modal, for accessibility. +* @param {string} modalHeading - The heading text displayed at the top of the modal. +* @param {string} modalDescription - The main descriptive text displayed within the modal. +* @param {string} modalSubmit - The HTML content for the submit button, allowing customization. +* @param {HTMLElement} wrapper_element - Optional. The element to which the modal is appended. If not provided, defaults to `document.body`. +* @param {boolean} forceAction - Optional. If true, adds a `data-force-action` attribute to the modal for additional control. +* +* The modal includes a heading, description, submit button, and a cancel button, along with a close button. +* The `data-close-modal` attribute is added to cancel and close buttons to enable closing functionality. +*/ +export function addModal(id, ariaLabelledby, ariaDescribedby, modalHeading, modalDescription, modalSubmit, wrapper_element, forceAction) { + + const modal = document.createElement('div'); + modal.setAttribute('class', 'usa-modal'); + modal.setAttribute('id', id); + modal.setAttribute('aria-labelledby', ariaLabelledby); + modal.setAttribute('aria-describedby', ariaDescribedby); + if (forceAction) + modal.setAttribute('data-force-action', ''); + + modal.innerHTML = ` +
        +
        +

        + ${modalHeading} +

        +
        +

        + ${modalDescription} +

        +
        + +
        + +
        + ` + if (wrapper_element) { + wrapper_element.appendChild(modal); + } else { + document.body.appendChild(modal); + } +} + +/** + * Helper function that creates a dynamic accordion navigation + * @param {string} action - The action type or identifier used to create a unique DOM IDs. + * @param {string} unique_id - An ID that when combined with action makes a unique identifier + * @param {string} modal_button_text - The action button's text + * @param {string} screen_reader_text - A screen reader helper + */ +export function generateKebabHTML(action, unique_id, modal_button_text, screen_reader_text) { + const generateModalButton = (mobileOnly = false) => ` +
        + ${mobileOnly ? `` : ''} + ${modal_button_text} + ${screen_reader_text} + + `; + + // Main kebab structure + const kebab = ` + ${generateModalButton(true)} +
        +
        + +
        + +
        + `; + + return kebab; +} + +export class BaseTable { constructor(sectionSelector) { - this.tableWrapper = document.getElementById(`${sectionSelector}__table-wrapper`); - this.tableHeaders = document.querySelectorAll(`#${sectionSelector} th[data-sortable]`); + this.itemName = itemName; + this.sectionSelector = itemName + 's'; + this.tableWrapper = document.getElementById(`${this.sectionSelector}__table-wrapper`); + this.tableHeaders = document.querySelectorAll(`#${this.sectionSelector} th[data-sortable]`); this.currentSortBy = 'id'; this.currentOrder = 'asc'; this.currentStatus = []; this.currentSearchTerm = ''; this.scrollToTable = false; - this.searchInput = document.getElementById(`${sectionSelector}__search-field`); - this.searchSubmit = document.getElementById(`${sectionSelector}__search-field-submit`); - this.tableAnnouncementRegion = document.getElementById(`${sectionSelector}__usa-table__announcement-region`); - this.resetSearchButton = document.getElementById(`${sectionSelector}__reset-search`); - this.resetFiltersButton = document.getElementById(`${sectionSelector}__reset-filters`); - this.statusCheckboxes = document.querySelectorAll(`.${sectionSelector} input[name="filter-status"]`); - this.statusIndicator = document.getElementById(`${sectionSelector}__filter-indicator`); - this.statusToggle = document.getElementById(`${sectionSelector}__usa-button--filter`); - this.noTableWrapper = document.getElementById(`${sectionSelector}__no-data`); - this.noSearchResultsWrapper = document.getElementById(`${sectionSelector}__no-search-results`); + this.searchInput = document.getElementById(`${this.sectionSelector}__search-field`); + this.searchSubmit = document.getElementById(`${this.sectionSelector}__search-field-submit`); + this.tableAnnouncementRegion = document.getElementById(`${this.sectionSelector}__usa-table__announcement-region`); + this.resetSearchButton = document.getElementById(`${this.sectionSelector}__reset-search`); + this.resetFiltersButton = document.getElementById(`${this.sectionSelector}__reset-filters`); + this.statusCheckboxes = document.querySelectorAll(`.${this.sectionSelector} input[name="filter-status"]`); + this.statusIndicator = document.getElementById(`${this.sectionSelector}__filter-indicator`); + this.statusToggle = document.getElementById(`${this.sectionSelector}__usa-button--filter`); + this.noTableWrapper = document.getElementById(`${this.sectionSelector}__no-data`); + this.noSearchResultsWrapper = document.getElementById(`${this.sectionSelector}__no-search-results`); this.portfolioElement = document.getElementById('portfolio-js-value'); this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; this.initializeTableHeaders(); @@ -31,31 +156,24 @@ export class LoadTableBase { } /** - * Generalized function to update pagination for a list. - * @param {string} itemName - The name displayed in the counter - * @param {string} paginationSelector - CSS selector for the pagination container. - * @param {string} counterSelector - CSS selector for the pagination counter. - * @param {string} tableSelector - CSS selector for the header element to anchor the links to. - * @param {number} currentPage - The current page number (starting with 1). - * @param {number} numPages - The total number of pages. - * @param {boolean} hasPrevious - Whether there is a page before the current page. - * @param {boolean} hasNext - Whether there is a page after the current page. - * @param {number} total - The total number of items. - */ + * Generalized function to update pagination for a list. + * @param {number} currentPage - The current page number (starting with 1). + * @param {number} numPages - The total number of pages. + * @param {boolean} hasPrevious - Whether there is a page before the current page. + * @param {boolean} hasNext - Whether there is a page after the current page. + * @param {number} total - The total number of items. + */ updatePagination( - itemName, - paginationSelector, - counterSelector, - parentTableSelector, currentPage, numPages, hasPrevious, hasNext, - totalItems, + totalItems ) { - const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); - const counterSelectorEl = document.querySelector(counterSelector); - const paginationSelectorEl = document.querySelector(paginationSelector); + const paginationButtons = document.querySelector(`#${this.sectionSelector}-pagination .usa-pagination__list`); + const counterSelectorEl = document.querySelector(`#${this.sectionSelector}-pagination .usa-pagination__counter`); + const paginationSelectorEl = document.querySelector(`#${this.sectionSelector}-pagination`); + const parentTableSelector = `#${this.sectionSelector}`; counterSelectorEl.innerHTML = ''; paginationButtons.innerHTML = ''; @@ -65,12 +183,30 @@ export class LoadTableBase { // Counter should only be displayed if there is more than 1 item paginationSelectorEl.classList.toggle('display-none', totalItems < 1); - counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`; + counterSelectorEl.innerHTML = `${totalItems} ${this.itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`; + + // Helper function to create a pagination item + const createPaginationItem = (page) => { + const paginationItem = document.createElement('li'); + paginationItem.classList.add('usa-pagination__item', 'usa-pagination__page-no'); + paginationItem.innerHTML = ` + ${page} + `; + if (page === currentPage) { + paginationItem.querySelector('a').classList.add('usa-current'); + paginationItem.querySelector('a').setAttribute('aria-current', 'page'); + } + paginationItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(page); + }); + return paginationItem; + }; if (hasPrevious) { - const prevPageItem = document.createElement('li'); - prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - prevPageItem.innerHTML = ` + const prevPaginationItem = document.createElement('li'); + prevPaginationItem.className = 'usa-pagination__item usa-pagination__arrow'; + prevPaginationItem.innerHTML = ` Previous `; - prevPageItem.querySelector('a').addEventListener('click', (event) => { + prevPaginationItem.querySelector('a').addEventListener('click', (event) => { event.preventDefault(); this.loadTable(currentPage - 1); }); - paginationButtons.appendChild(prevPageItem); + paginationButtons.appendChild(prevPaginationItem); } // Add first page and ellipsis if necessary if (currentPage > 2) { - paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage)); + paginationButtons.appendChild(createPaginationItem(1)); if (currentPage > 3) { const ellipsis = document.createElement('li'); ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; @@ -99,7 +235,7 @@ export class LoadTableBase { // Add pages around the current page for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { - paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage)); + paginationButtons.appendChild(createPaginationItem(i)); } // Add last page and ellipsis if necessary @@ -111,13 +247,13 @@ export class LoadTableBase { ellipsis.innerHTML = ''; paginationButtons.appendChild(ellipsis); } - paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage)); + paginationButtons.appendChild(createPaginationItem(numPages)); } if (hasNext) { - const nextPageItem = document.createElement('li'); - nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - nextPageItem.innerHTML = ` + const nextPaginationItem = document.createElement('li'); + nextPaginationItem.className = 'usa-pagination__item usa-pagination__arrow'; + nextPaginationItem.innerHTML = ` Next `; - nextPageItem.querySelector('a').addEventListener('click', (event) => { + nextPaginationItem.querySelector('a').addEventListener('click', (event) => { event.preventDefault(); this.loadTable(currentPage + 1); }); - paginationButtons.appendChild(nextPageItem); + paginationButtons.appendChild(nextPaginationItem); } } /** - * A helper that toggles content/ no content/ no search results - * + * A helper that toggles content/ no content/ no search results based on results in data. + * @param {Object} data - Data representing current page of results data. + * @param {HTMLElement} dataWrapper - The DOM element to show if there are results on the current page. + * @param {HTMLElement} noDataWrapper - The DOM element to show if there are no results period. + * @param {HTMLElement} noSearchResultsWrapper - The DOM element to show if there are no results in the current filtered search. */ updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { const { unfiltered_total, total } = data; @@ -156,24 +295,6 @@ export class LoadTableBase { } }; - // Helper function to create a page item - createPageItem(page, parentTableSelector, currentPage) { - const pageItem = document.createElement('li'); - pageItem.className = 'usa-pagination__item usa-pagination__page-no'; - pageItem.innerHTML = ` - ${page} - `; - if (page === currentPage) { - pageItem.querySelector('a').classList.add('usa-current'); - pageItem.querySelector('a').setAttribute('aria-current', 'page'); - } - pageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - this.loadTable(page); - }); - return pageItem; - } - /** * A helper that resets sortable table headers * @@ -187,9 +308,181 @@ export class LoadTableBase { header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); }; - // Abstract method (to be implemented in the child class) - loadTable(page, sortBy, order) { - throw new Error('loadData() must be implemented in a subclass'); + /** + * Generates search params for filtering and sorting + * @param {number} page - The current page number for pagination (starting with 1) + * @param {*} sortBy - The sort column option + * @param {*} order - The order of sorting {asc, desc} + * @param {string} searchTerm - The search term used to filter results for a specific keyword + * @param {*} status - The status filter applied {ready, dns_needed, etc} + * @param {string} portfolio - The portfolio id + */ + getSearchParams(page, sortBy, order, searchTerm, status, portfolio) { + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "search_term": searchTerm, + } + ); + + let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null; + let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null; + let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null; + + if (portfolio) + searchParams.append("portfolio", portfolio); + if (emailValue) + searchParams.append("email", emailValue); + if (memberIdValue) + searchParams.append("member_id", memberIdValue); + if (memberOnly) + searchParams.append("member_only", memberOnly); + if (status) + searchParams.append("status", status); + return searchParams; + } + + /** + * Gets the base URL of API requests + * Placeholder function in a parent class - method should be implemented by child class for specifics + * Throws an error if called directly from the parent class + */ + getBaseUrl() { + throw new Error('getBaseUrl must be defined'); + } + + /** + * Calls "uswdsUnloadModals" to remove any existing modal element to make sure theres no unintended consequences + * from leftover event listeners + can be properly re-initialized + */ + uswdsUnloadModals(){} + + /** + * Loads modals + sets up event listeners for the modal submit actions + * "Activates" the modals after the DOM updates + * Utilizes "uswdsInitializeModals" + * Adds click event listeners to each modal's submit button so we can handle a user's actions + * + * When the submit button is clicked: + * - Triggers the close button to reset modal classes + * - Determines if the page needs refreshing if the last item is deleted + * @param {number} page - The current page number for pagination + * @param {number} total - The total # of items on the current page + * @param {number} unfiltered_total - The total # of items across all pages + */ + loadModals(page, total, unfiltered_total) {} + + /** + * Allows us to customize the table display based on specific conditions and a user's permissions + * Dynamically manages the visibility set up of columns, adding/removing headers + * (ie if a domain request is deleteable, we include the kebab column or if a user has edit permissions + * for a member, they will also see the kebab column) + * @param {Object} dataObjects - Data which contains info on domain requests or a user's permission + * Currently returns a dictionary of either: + * - "needsAdditionalColumn": If a new column should be displayed + * - "UserPortfolioPermissionChoices": A user's portfolio permission choices + */ + customizeTable(dataObjects){ return {}; } + + /** + * Retrieves specific data objects + * Placeholder function in a parent class - method should be implemented by child class for specifics + * Throws an error if called directly from the parent class + * Returns either: data.members, data.domains or data.domain_requests + * @param {Object} data - The full data set from which a subset of objects is extracted. + */ + getDataObjects(data) { + throw new Error('getDataObjects must be defined'); + } + + /** + * Creates + appends a row to a tbody element + * Tailored structure set up for each data object (domain, domain_request, member, etc) + * Placeholder function in a parent class - method should be implemented by child class for specifics + * Throws an error if called directly from the parent class + * Returns either: data.members, data.domains or data.domain_requests + * @param {Object} dataObject - The data used to populate the row content + * @param {HTMLElement} tbody - The table body to which the new row is appended to + * @param {Object} customTableOptions - Additional options for customizing row appearance (ie needsAdditionalColumn) + */ + addRow(dataObject, tbody, customTableOptions) { + throw new Error('addRow must be defined'); + } + + /** + * See function for more details + */ + initShowMoreButtons(){} + + /** + * Loads rows in the members list, as well as updates pagination around the members list + * based on the supplied attributes. + * @param {*} page - The page number of the results (starts with 1) + * @param {*} sortBy - The sort column option + * @param {*} order - The sort order {asc, desc} + * @param {*} scroll - The control for the scrollToElement functionality + * @param {*} searchTerm - The search term + * @param {*} portfolio - The portfolio id + */ + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + // --------- SEARCH + let searchParams = this.getSearchParams(page, sortBy, order, searchTerm, status, portfolio); + + // --------- FETCH DATA + // fetch json of page of domains, given params + const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null; + if (!baseUrlValue) return; + + let url = `${baseUrlValue}?${searchParams.toString()}` + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; + } + + // handle the display of proper messaging in the event that no members exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + // identify the DOM element where the list of results will be inserted into the DOM + const tbody = this.tableWrapper.querySelector('tbody'); + tbody.innerHTML = ''; + + // remove any existing modal elements from the DOM so they can be properly re-initialized + // after the DOM content changes and there are new delete modal buttons added + this.unloadModals(); + + let dataObjects = this.getDataObjects(data); + let customTableOptions = this.customizeTable(data); + + dataObjects.forEach(dataObject => { + this.addRow(dataObject, tbody, customTableOptions); + }); + + this.initShowMoreButtons(); + + this.loadModals(data.page, data.total, data.unfiltered_total); + + // Do not scroll on first page load + if (scroll) + scrollToElement('class', this.sectionSelector); + this.scrollToTable = true; + + // update pagination + this.updatePagination( + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; + }) + .catch(error => console.error('Error fetching objects:', error)); } // Add event listeners to table headers for sorting diff --git a/src/registrar/assets/modules/table-domain-requests.js b/src/registrar/assets/modules/table-domain-requests.js index 55b2c951c..e408bbd1b 100644 --- a/src/registrar/assets/modules/table-domain-requests.js +++ b/src/registrar/assets/modules/table-domain-requests.js @@ -1,8 +1,8 @@ -import { hideElement, showElement, scrollToElement } from './helpers.js'; -import { initializeModals, unloadModals } from './helpers-uswds.js'; +import { hideElement, showElement } from './helpers.js'; +import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js'; import { getCsrfToken } from './helpers-csrf-token.js'; -import { LoadTableBase } from './table-base.js'; +import { BaseTable, addModal, generateKebabHTML } from './table-base.js'; const utcDateString = (dateString) => { @@ -20,10 +20,14 @@ const utcDateString = (dateString) => { }; -export class DomainRequestsTable extends LoadTableBase { +export class DomainRequestsTable extends BaseTable { constructor() { - super('domain-requests'); + super('domain-request'); + } + + getBaseUrl() { + return document.getElementById("get_domain_requests_json_url"); } toggleExportButton(requests) { @@ -35,327 +39,137 @@ export class DomainRequestsTable extends LoadTableBase { hideElement(exportButton); } } -} + } - /** - * Loads rows in the domains list, as well as updates pagination around the domains list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} status - control for the status filter - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) { - let baseUrl = document.getElementById("get_domain_requests_json_url"); - - if (!baseUrl) { - return; - } + getDataObjects(data) { + return data.domain_requests; + } + unloadModals() { + uswdsUnloadModals(); + } + customizeTable(data) { - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } + // Manage "export as CSV" visibility for domain requests + this.toggleExportButton(data.domain_requests); - // add searchParams - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "status": status, - "search_term": searchTerm + let needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); + + // Remove existing delete th and td if they exist + let existingDeleteTh = document.querySelector('.delete-header'); + if (!needsDeleteColumn) { + if (existingDeleteTh) + existingDeleteTh.remove(); + } else { + if (!existingDeleteTh) { + const delheader = document.createElement('th'); + delheader.setAttribute('scope', 'col'); + delheader.setAttribute('role', 'columnheader'); + delheader.setAttribute('class', 'delete-header width-5'); + delheader.innerHTML = ` + Delete Action`; + let tableHeaderRow = this.tableWrapper.querySelector('thead tr'); + tableHeaderRow.appendChild(delheader); } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) + } + return { 'needsAdditionalColumn': needsDeleteColumn }; + } - let url = `${baseUrlValue}?${searchParams.toString()}` - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; + addRow(dataObject, tbody, customTableOptions) { + const request = dataObject; + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const domainName = request.requested_domain ? request.requested_domain : `New domain request
        (${utcDateString(request.created_at)})`; + const actionUrl = request.action_url; + const actionLabel = request.action_label; + const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; + + // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) + // If the request is not deletable, use the following (hidden) span for ANDI screenreaders to indicate this state to the end user + let modalTrigger = ` + Domain request cannot be deleted now. Edit the request for more information.`; + + let markupCreatorRow = ''; + + if (this.portfolioValue) { + markupCreatorRow = ` + + ${request.creator ? request.creator : ''} + + ` + } + + if (request.is_deletable) { + // 1st path: Just a modal trigger in any screen size for non-org users + modalTrigger = ` + + Delete ${domainName} + ` + + // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly + if (this.portfolioValue) { + + // 2nd path: Just a modal trigger on mobile for org users or kebab + accordion with nested modal trigger on desktop for org users + modalTrigger = generateKebabHTML('delete-domain', request.id, 'Delete', domainName); + } + } + + const row = document.createElement('tr'); + row.innerHTML = ` + + ${domainName} + + + ${submissionDate} + + ${markupCreatorRow} + + ${request.status} + + + + + ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} + + + ${customTableOptions.needsAdditionalColumn ? ''+modalTrigger+'' : ''} + `; + tbody.appendChild(row); + if (request.is_deletable) DomainRequestsTable.addDomainRequestsModal(request.requested_domain, request.id, request.created_at, tbody); + } + + loadModals(page, total, unfiltered_total) { + // initialize modals immediately after the DOM content is updated + uswdsInitializeModals(); + + // Now the DOM and modals are ready, add listeners to the submit buttons + const modals = document.querySelectorAll('.usa-modal__content'); + + modals.forEach(modal => { + const submitButton = modal.querySelector('.usa-modal__submit'); + const closeButton = modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + let pk = submitButton.getAttribute('data-pk'); + // Workaround: Close the modal to remove the USWDS UI local classes + closeButton.click(); + // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page + let pageToDisplay = page; + if (total == 1 && unfiltered_total > 1) { + pageToDisplay--; } - // Manage "export as CSV" visibility for domain requests - this.toggleExportButton(data.domain_requests); - - // handle the display of proper messaging in the event that no requests exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain request list will be inserted into the DOM - const tbody = document.querySelector('#domain-requests tbody'); - tbody.innerHTML = ''; - - // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases - // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, - // which will cause bad delete requests to be sent. - const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); - preExistingModalPlaceholders.forEach(element => { - element.remove(); - }); - - // remove any existing modal elements from the DOM so they can be properly re-initialized - // after the DOM content changes and there are new delete modal buttons added - unloadModals(); - - let needsDeleteColumn = false; - - needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); - - // Remove existing delete th and td if they exist - let existingDeleteTh = document.querySelector('.delete-header'); - if (!needsDeleteColumn) { - if (existingDeleteTh) - existingDeleteTh.remove(); - } else { - if (!existingDeleteTh) { - const delheader = document.createElement('th'); - delheader.setAttribute('scope', 'col'); - delheader.setAttribute('role', 'columnheader'); - delheader.setAttribute('class', 'delete-header'); - delheader.innerHTML = ` - Delete Action`; - let tableHeaderRow = document.querySelector('#domain-requests thead tr'); - tableHeaderRow.appendChild(delheader); - } - } - - data.domain_requests.forEach(request => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const domainName = request.requested_domain ? request.requested_domain : `New domain request
        (${utcDateString(request.created_at)})`; - const actionUrl = request.action_url; - const actionLabel = request.action_label; - const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; - - // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) - // If the request is not deletable, use the following (hidden) span for ANDI screenreaders to indicate this state to the end user - let modalTrigger = ` - Domain request cannot be deleted now. Edit the request for more information.`; - - let markupCreatorRow = ''; - - if (this.portfolioValue) { - markupCreatorRow = ` - - ${request.creator ? request.creator : ''} - - ` - } - - if (request.is_deletable) { - // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages - let modalHeading = ''; - let modalDescription = ''; - - if (request.requested_domain) { - modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } else { - if (request.created_at) { - modalHeading = 'Are you sure you want to delete this domain request?'; - modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; - } else { - modalHeading = 'Are you sure you want to delete New domain request?'; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } - } - - modalTrigger = ` - - Delete ${domainName} - ` - - const modalSubmit = ` - - ` - - const modal = document.createElement('div'); - modal.setAttribute('class', 'usa-modal'); - modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); - modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); - modal.setAttribute('aria-describedby', 'Domain will be removed'); - modal.setAttribute('data-force-action', ''); - - modal.innerHTML = ` -
        -
        - -
        - -
        - -
        - -
        - ` - - this.tableWrapper.appendChild(modal); - - // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly - if (this.portfolioValue) { - modalTrigger = ` - - Delete ${domainName} - - -
        -
        - -
        - -
        - ` - } - } - - - const row = document.createElement('tr'); - row.innerHTML = ` - - ${domainName} - - - ${submissionDate} - - ${markupCreatorRow} - - ${request.status} - - - - - ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} - - - ${needsDeleteColumn ? ''+modalTrigger+'' : ''} - `; - tbody.appendChild(row); - }); - - // initialize modals immediately after the DOM content is updated - initializeModals(); - - // Now the DOM and modals are ready, add listeners to the submit buttons - const modals = document.querySelectorAll('.usa-modal__content'); - - modals.forEach(modal => { - const submitButton = modal.querySelector('.usa-modal__submit'); - const closeButton = modal.querySelector('.usa-modal__close'); - submitButton.addEventListener('click', () => { - let pk = submitButton.getAttribute('data-pk'); - // Close the modal to remove the USWDS UI local classes - closeButton.click(); - // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page - let pageToDisplay = data.page; - if (data.total == 1 && data.unfiltered_total > 1) { - pageToDisplay--; - } - this.deleteDomainRequest(pk, pageToDisplay); - }); - }); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'domain-requests'); - this.scrollToTable = true; - - // update the pagination after the domain requests list is updated - this.updatePagination( - 'domain request', - '#domain-requests-pagination', - '#domain-requests-pagination .usa-pagination__counter', - '#domain-requests', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domain requests:', error)); + this.deleteDomainRequest(pk, pageToDisplay); + }); + }); } /** @@ -385,10 +199,46 @@ export class DomainRequestsTable extends LoadTableBase { throw new Error(`HTTP error! status: ${response.status}`); } // Update data and UI - this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentStatus, this.currentSearchTerm); }) .catch(error => console.error('Error fetching domain requests:', error)); } + + /** + * Modal that displays when deleting a domain request + * @param {string} requested_domain - The requested domain URL + * @param {string} id - The request's ID + * @param {string}} created_at - When the request was created at + * @param {HTMLElement} wrapper_element - The element to which the modal is appended + */ + static addDomainRequestsModal(requested_domain, id, created_at, wrapper_element) { + // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages + let modalHeading = ''; + let modalDescription = ''; + + if (requested_domain) { + modalHeading = `Are you sure you want to delete ${requested_domain}?`; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + } else { + if (request.created_at) { + modalHeading = 'Are you sure you want to delete this domain request?'; + modalDescription = `This will remove the domain request (created ${utcDateString(created_at)}) from the .gov registrar. This action cannot be undone`; + } else { + modalHeading = 'Are you sure you want to delete New domain request?'; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + } + } + + const modalSubmit = ` + + ` + + addModal(`toggle-delete-domain-${id}`, 'Are you sure you want to continue?', 'Domain will be removed', modalHeading, modalDescription, modalSubmit, wrapper_element, true); + + } } export function initDomainRequestsTable() { diff --git a/src/registrar/assets/modules/table-domains.js b/src/registrar/assets/modules/table-domains.js index 30677a60d..20d9ef7de 100644 --- a/src/registrar/assets/modules/table-domains.js +++ b/src/registrar/assets/modules/table-domains.js @@ -1,143 +1,67 @@ -import { scrollToElement } from './helpers.js'; -import { initializeTooltips } from './helpers-uswds.js'; +import { BaseTable } from './table-base.js'; -import { LoadTableBase } from './table-base.js'; - -export class DomainsTable extends LoadTableBase { +export class DomainsTable extends BaseTable { constructor() { - super('domains'); + super('domain'); } - /** - * Loads rows in the domains list, as well as updates pagination around the domains list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} status - control for the status filter - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + getBaseUrl() { + return document.getElementById("get_domains_json_url"); + } + getDataObjects(data) { + return data.domains; + } + addRow(dataObject, tbody, customTableOptions) { + const domain = dataObject; + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null; + const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : ''; + const expirationDateSortValue = expirationDate ? expirationDate.getTime() : ''; + const actionUrl = domain.action_url; + const suborganization = domain.domain_info__sub_organization ? domain.domain_info__sub_organization : '⎯'; - // fetch json of page of domais, given params - let baseUrl = document.getElementById("get_domains_json_url"); - if (!baseUrl) { - return; + const row = document.createElement('tr'); + + let markupForSuborganizationRow = ''; + + if (this.portfolioValue) { + markupForSuborganizationRow = ` + + ${suborganization} + + ` } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - // fetch json of page of domains, given params - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "status": status, - "search_term": searchTerm - } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) - - let url = `${baseUrlValue}?${searchParams.toString()}` - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no domains exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const domainList = document.querySelector('#domains tbody'); - domainList.innerHTML = ''; - - data.domains.forEach(domain => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null; - const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : ''; - const expirationDateSortValue = expirationDate ? expirationDate.getTime() : ''; - const actionUrl = domain.action_url; - const suborganization = domain.domain_info__sub_organization ? domain.domain_info__sub_organization : '⎯'; - - const row = document.createElement('tr'); - - let markupForSuborganizationRow = ''; - - if (this.portfolioValue) { - markupForSuborganizationRow = ` - - ${suborganization} - - ` - } - - row.innerHTML = ` - - ${domain.name} - - - ${expirationDateFormatted} - - - ${domain.state_display} - - - - - ${markupForSuborganizationRow} - - - - ${domain.action_label} ${domain.name} - - - `; - domainList.appendChild(row); - }); - // initialize tool tips immediately after the associated DOM elements are added - initializeTooltips(); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'domains'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'domain', - '#domains-pagination', - '#domains-pagination .usa-pagination__counter', - '#domains', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domains:', error)); + row.innerHTML = ` + + ${domain.name} + + + ${expirationDateFormatted} + + + ${domain.state_display} + + + + + ${markupForSuborganizationRow} + + + + ${domain.action_label} ${domain.name} + + + `; + tbody.appendChild(row); } } diff --git a/src/registrar/assets/modules/table-member-domains.js b/src/registrar/assets/modules/table-member-domains.js index 1794c72d4..7d235f6e5 100644 --- a/src/registrar/assets/modules/table-member-domains.js +++ b/src/registrar/assets/modules/table-member-domains.js @@ -1,112 +1,29 @@ -import { scrollToElement } from './helpers.js'; -import { LoadTableBase } from './table-base.js'; +import { BaseTable } from './table-base.js'; -export class MemberDomainsTable extends LoadTableBase { +export class MemberDomainsTable extends BaseTable { constructor() { - super('member-domains'); + super('member-domain'); this.currentSortBy = 'name'; } - /** - * Loads rows in the members list, as well as updates pagination around the members list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - - // --------- SEARCH - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "search_term": searchTerm, - } - ); - - let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null; - let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null; - let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null; - - if (portfolio) - searchParams.append("portfolio", portfolio) - if (emailValue) - searchParams.append("email", emailValue) - if (memberIdValue) - searchParams.append("member_id", memberIdValue) - if (memberOnly) - searchParams.append("member_only", memberOnly) - - - // --------- FETCH DATA - // fetch json of page of domais, given params - let baseUrl = document.getElementById("get_member_domains_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no members exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const memberDomainsList = document.querySelector('#member-domains tbody'); - memberDomainsList.innerHTML = ''; - - - data.domains.forEach(domain => { - const row = document.createElement('tr'); - - row.innerHTML = ` - - ${domain.name} - - `; - memberDomainsList.appendChild(row); - }); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'member-domains'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'member domain', - '#member-domains-pagination', - '#member-domains-pagination .usa-pagination__counter', - '#member-domains', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domains:', error)); + getBaseUrl() { + return document.getElementById("get_member_domains_json_url"); } + getDataObjects(data) { + return data.domains; + } + addRow(dataObject, tbody, customTableOptions) { + const domain = dataObject; + const row = document.createElement('tr'); + row.innerHTML = ` + + ${domain.name} + + `; + tbody.appendChild(row); + } + } export function initMemberDomainsTable() { diff --git a/src/registrar/assets/modules/table-members.js b/src/registrar/assets/modules/table-members.js index f75b4c9e3..d3bc1294b 100644 --- a/src/registrar/assets/modules/table-members.js +++ b/src/registrar/assets/modules/table-members.js @@ -1,13 +1,145 @@ -import { hideElement, showElement, scrollToElement } from './helpers.js'; +import { hideElement, showElement } from './helpers.js'; -import { LoadTableBase } from './table-base.js'; +import { BaseTable, addModal, generateKebabHTML } from './table-base.js'; -export class MembersTable extends LoadTableBase { +export class MembersTable extends BaseTable { constructor() { - super('members'); + super('member'); } - + + getBaseUrl() { + return document.getElementById("get_members_json_url"); + } + + // Abstract method (to be implemented in the child class) + getDataObjects(data) { + return data.members; + } + unloadModals() { + uswdsUnloadModals(); + } + loadModals(page, total, unfiltered_total) { + // initialize modals immediately after the DOM content is updated + uswdsInitializeModals(); + + // Now the DOM and modals are ready, add listeners to the submit buttons + const modals = document.querySelectorAll('.usa-modal__content'); + + modals.forEach(modal => { + const submitButton = modal.querySelector('.usa-modal__submit'); + const closeButton = modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + let pk = submitButton.getAttribute('data-pk'); + // Close the modal to remove the USWDS UI local classes + closeButton.click(); + // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page + let pageToDisplay = page; + if (total == 1 && unfiltered_total > 1) { + pageToDisplay--; + } + + this.deleteMember(pk, pageToDisplay); + }); + }); + } + + customizeTable(data) { + // Get whether the logged in user has edit members permission + const hasEditPermission = this.portfolioElement ? this.portfolioElement.getAttribute('data-has-edit-permission')==='True' : null; + + let existingExtraActionsHeader = document.querySelector('.extra-actions-header'); + + if (hasEditPermission && !existingExtraActionsHeader) { + const extraActionsHeader = document.createElement('th'); + extraActionsHeader.setAttribute('id', 'extra-actions'); + extraActionsHeader.setAttribute('role', 'columnheader'); + extraActionsHeader.setAttribute('class', 'extra-actions-header width-5'); + extraActionsHeader.innerHTML = ` + Extra Actions`; + let tableHeaderRow = this.tableWrapper.querySelector('thead tr'); + tableHeaderRow.appendChild(extraActionsHeader); + } + return { + 'needsAdditionalColumn': hasEditPermission, + 'UserPortfolioPermissionChoices' : data.UserPortfolioPermissionChoices + }; + } + + addRow(dataObject, tbody, customTableOptions) { + const member = dataObject; + // member is based on either a UserPortfolioPermission or a PortfolioInvitation + // and also includes information from related domains; the 'id' of the org_member + // is the id of the UserPorfolioPermission or PortfolioInvitation, it is not a user id + // member.type is either invitedmember or member + const unique_id = member.type + member.id; // unique string for use in dom, this is + // not the id of the associated user + const member_delete_url = member.action_url + "/delete"; + const num_domains = member.domain_urls.length; + const last_active = this.handleLastActive(member.last_active); + let cancelInvitationButton = member.type === "invitedmember" ? "Cancel invitation" : "Remove member"; + const kebabHTML = customTableOptions.needsAdditionalColumn ? generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member.name}`): ''; + + const row = document.createElement('tr'); + + let admin_tagHTML = ``; + if (member.is_admin) + admin_tagHTML = `Admin` + + // generate html blocks for domains and permissions for the member + let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url); + let permissionsHTML = this.generatePermissionsHTML(member.permissions, customTableOptions.UserPortfolioPermissionChoices); + + // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand + let showMoreButton = ''; + const showMoreRow = document.createElement('tr'); + if (domainsHTML || permissionsHTML) { + showMoreButton = ` + + `; + + showMoreRow.innerHTML = `
        ${domainsHTML} ${permissionsHTML}
        `; + showMoreRow.classList.add('show-more-content'); + showMoreRow.classList.add('display-none'); + showMoreRow.id = unique_id; + } + + row.innerHTML = ` + + ${member.member_display} ${admin_tagHTML} ${showMoreButton} + + + ${last_active.display_value} + + + + + ${member.action_label} ${member.name} + + + ${customTableOptions.needsAdditionalColumn ? ''+kebabHTML+'' : ''} + `; + tbody.appendChild(row); + if (domainsHTML || permissionsHTML) { + tbody.appendChild(showMoreRow); + } + // This easter egg is only for fixtures that dont have names as we are displaying their emails + // All prod users will have emails linked to their account + if (customTableOptions.needsAdditionalColumn) MembersTable.addMemberModal(num_domains, member.email || "Samwise Gamgee", member_delete_url, unique_id, row); + } + /** * Initializes "Show More" buttons on the page, enabling toggle functionality to show or hide content. * @@ -135,6 +267,86 @@ export class MembersTable extends LoadTableBase { return domainsHTML; } + /** + * The POST call for deleting a Member and which error or success message it should return + * and redirection if necessary + * + * @param {string} member_delete_url - The URL for deletion ie `${member_type}-${member_id}/delete`` + * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page + * Note: X-Request-With is used for security reasons to present CSRF attacks, the server checks that this header is present + * (consent via CORS) so it knows it's not from a random request attempt + */ + deleteMember(member_delete_url, pageToDisplay) { + // Get CSRF token + const csrfToken = getCsrfToken(); + // Create FormData object and append the CSRF token + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}`; + + fetch(`${member_delete_url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': csrfToken, + }, + body: formData + }) + .then(response => { + if (response.status === 200) { + response.json().then(data => { + if (data.success) { + this.addAlert("success", data.success); + } + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentStatus, this.currentSearchTerm); + }); + } else { + response.json().then(data => { + if (data.error) { + // This should display the error given from backend for + // either only admin OR in progress requests + this.addAlert("error", data.error); + } else { + throw new Error(`Unexpected status: ${response.status}`); + } + }); + } + }) + .catch(error => { + console.error('Error deleting member:', error); + }); + } + + + /** + * Adds an alert message to the page with an alert class. + * + * @param {string} alertClass - {error, warning, info, success} + * @param {string} alertMessage - The text that will be displayed + * + */ + addAlert(alertClass, alertMessage) { + let toggleableAlertDiv = document.getElementById("toggleable-alert"); + this.resetAlerts(); + toggleableAlertDiv.classList.add(`usa-alert--${alertClass}`); + let alertParagraph = toggleableAlertDiv.querySelector(".usa-alert__text"); + alertParagraph.innerHTML = alertMessage + showElement(toggleableAlertDiv); + } + + /** + * Resets the reusable alert message + */ + resetAlerts() { + // Create a list of any alert that's leftover and remove + document.querySelectorAll(".usa-alert:not(#toggleable-alert)").forEach(alert => { + alert.remove(); + }); + let toggleableAlertDiv = document.getElementById("toggleable-alert"); + toggleableAlertDiv.classList.remove('usa-alert--error'); + toggleableAlertDiv.classList.remove('usa-alert--success'); + hideElement(toggleableAlertDiv); + } + /** * Generates an HTML string summarizing a user's additional permissions within a portfolio, * based on the user's permissions and predefined permission choices. @@ -199,157 +411,40 @@ export class MembersTable extends LoadTableBase { } /** - * Loads rows in the members list, as well as updates pagination around the members list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + * Modal that displays when deleting a domain request + * @param {string} num_domains - Number of domain a user has within the org + * @param {string} member_email - The member's email + * @param {string} submit_delete_url - `${member_type}-${member_id}/delete` + * @param {HTMLElement} wrapper_element - The element to which the modal is appended + */ + static addMemberModal(num_domains, member_email, submit_delete_url, id, wrapper_element) { + let modalHeading = ''; + let modalDescription = ''; - // --------- SEARCH - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "search_term": searchTerm - } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) + if (num_domains == 0){ + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `They will no longer be able to access this organization. + This action cannot be undone.`; + } else if (num_domains == 1) { + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `${member_email} currently manages ${num_domains} domain in the organization. + Removing them from the organization will remove all of their domains. They will no longer be able to + access this organization. This action cannot be undone.`; + } else if (num_domains > 1) { + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `${member_email} currently manages ${num_domains} domains in the organization. + Removing them from the organization will remove all of their domains. They will no longer be able to + access this organization. This action cannot be undone.`; + } + const modalSubmit = ` + + ` - // --------- FETCH DATA - // fetch json of page of domains, given params - let baseUrl = document.getElementById("get_members_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no members exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const memberList = document.querySelector('#members tbody'); - memberList.innerHTML = ''; - - const UserPortfolioPermissionChoices = data.UserPortfolioPermissionChoices; - const invited = 'Invited'; - const invalid_date = 'Invalid date'; - - data.members.forEach(member => { - const member_id = member.source + member.id; - const member_name = member.name; - const member_display = member.member_display; - const member_permissions = member.permissions; - const domain_urls = member.domain_urls; - const domain_names = member.domain_names; - const num_domains = domain_urls.length; - - const last_active = this.handleLastActive(member.last_active); - - const action_url = member.action_url; - const action_label = member.action_label; - const svg_icon = member.svg_icon; - - const row = document.createElement('tr'); - - let admin_tagHTML = ``; - if (member.is_admin) - admin_tagHTML = `Admin` - - // generate html blocks for domains and permissions for the member - let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, action_url); - let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices); - - // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand - let showMoreButton = ''; - const showMoreRow = document.createElement('tr'); - if (domainsHTML || permissionsHTML) { - showMoreButton = ` - - `; - - showMoreRow.innerHTML = `
        ${domainsHTML} ${permissionsHTML}
        `; - showMoreRow.classList.add('show-more-content'); - showMoreRow.classList.add('display-none'); - showMoreRow.id = member_id; - } - - row.innerHTML = ` - - ${member_display} ${admin_tagHTML} ${showMoreButton} - - - ${last_active.display_value} - - - - - ${action_label} ${member_name} - - - `; - memberList.appendChild(row); - if (domainsHTML || permissionsHTML) { - memberList.appendChild(showMoreRow); - } - }); - - this.initShowMoreButtons(); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'members'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'member', - '#members-pagination', - '#members-pagination .usa-pagination__counter', - '#members', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching members:', error)); + addModal(`toggle-remove-member-${id}`, 'Are you sure you want to continue?', 'Member will be removed', modalHeading, modalDescription, modalSubmit, wrapper_element, true); } } From de76280a722546a1ae60c981199e475236ef645d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 19:43:11 -0500 Subject: [PATCH 095/148] Fix function calls --- src/registrar/assets/modules/main.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/modules/main.js b/src/registrar/assets/modules/main.js index cd89540ab..4774b4b41 100644 --- a/src/registrar/assets/modules/main.js +++ b/src/registrar/assets/modules/main.js @@ -30,12 +30,12 @@ hookupYesNoListener("additional_details-has_cisa_representative",'cisa-represent initializeUrbanizationToggle(); -userProfileListener; -finishUserSetupListener; +userProfileListener(); +finishUserSetupListener(); -loadInitialValuesForComboBoxes; +loadInitialValuesForComboBoxes(); -handleRequestingEntityFieldset; +handleRequestingEntityFieldset(); initDomainsTable(); initDomainRequestsTable(); From 90a608c0e7ef9f0295381b4524e4c94123c87b13 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 20:00:58 -0500 Subject: [PATCH 096/148] Hand merge 3100 --- src/registrar/assets/modules/table-domain-requests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/modules/table-domain-requests.js b/src/registrar/assets/modules/table-domain-requests.js index e408bbd1b..5d5265b88 100644 --- a/src/registrar/assets/modules/table-domain-requests.js +++ b/src/registrar/assets/modules/table-domain-requests.js @@ -220,7 +220,7 @@ export class DomainRequestsTable extends BaseTable { modalHeading = `Are you sure you want to delete ${requested_domain}?`; modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; } else { - if (request.created_at) { + if (created_at) { modalHeading = 'Are you sure you want to delete this domain request?'; modalDescription = `This will remove the domain request (created ${utcDateString(created_at)}) from the .gov registrar. This action cannot be undone`; } else { From 74ef42095e38ad02fbf0ed811674b398c571feeb Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 20:05:07 -0500 Subject: [PATCH 097/148] Fix param for BaseTable --- src/registrar/assets/modules/table-base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/modules/table-base.js b/src/registrar/assets/modules/table-base.js index 84cf64663..f2ea351e3 100644 --- a/src/registrar/assets/modules/table-base.js +++ b/src/registrar/assets/modules/table-base.js @@ -124,7 +124,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r } export class BaseTable { - constructor(sectionSelector) { + constructor(itemName) { this.itemName = itemName; this.sectionSelector = itemName + 's'; this.tableWrapper = document.getElementById(`${this.sectionSelector}__table-wrapper`); From e24a30ecb3acaaafd89eea67c3da4b689165a997 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 20:22:33 -0500 Subject: [PATCH 098/148] Fix function name --- src/registrar/assets/modules/table-base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/modules/table-base.js b/src/registrar/assets/modules/table-base.js index f2ea351e3..07b7cff5e 100644 --- a/src/registrar/assets/modules/table-base.js +++ b/src/registrar/assets/modules/table-base.js @@ -357,7 +357,7 @@ export class BaseTable { * Calls "uswdsUnloadModals" to remove any existing modal element to make sure theres no unintended consequences * from leftover event listeners + can be properly re-initialized */ - uswdsUnloadModals(){} + unloadModals(){} /** * Loads modals + sets up event listeners for the modal submit actions From a61f81022386eeb9c0b812dcaf256c5c551b5125 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 20:29:01 -0500 Subject: [PATCH 099/148] add missing dependency to table members --- src/registrar/assets/modules/table-members.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/modules/table-members.js b/src/registrar/assets/modules/table-members.js index d3bc1294b..25fc3de74 100644 --- a/src/registrar/assets/modules/table-members.js +++ b/src/registrar/assets/modules/table-members.js @@ -1,5 +1,5 @@ import { hideElement, showElement } from './helpers.js'; - +import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js'; import { BaseTable, addModal, generateKebabHTML } from './table-base.js'; export class MembersTable extends BaseTable { From e6035ed0b2d68e512d9088dad542e07c17ada187 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 20:37:22 -0500 Subject: [PATCH 100/148] remove extra scss that only produces a console log error --- src/registrar/assets/sass/_theme/_alerts.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/sass/_theme/_alerts.scss index 3cfa768fe..9579cc057 100644 --- a/src/registrar/assets/sass/_theme/_alerts.scss +++ b/src/registrar/assets/sass/_theme/_alerts.scss @@ -47,7 +47,3 @@ background-color: color('base-darkest'); } } - -.usa-site-alert--hot-pink .usa-alert .usa-alert__body::before { - background-image: url('../img/usa-icons-bg/error.svg'); -} From 1445e1bf61e592b89890f68221df98c50f2b8dc3 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 20:52:59 -0500 Subject: [PATCH 101/148] Fix unit tests --- src/registrar/tests/test_admin.py | 4 +++- src/registrar/tests/test_admin_request.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 2677462df..84b85e412 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -654,7 +654,9 @@ class TestDomainInformationAdmin(TestCase): self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) # Test for the copy link - self.assertContains(response, "copy-to-clipboard", count=3) + # We expect 3 in the form + 2 from the js module copy-to-clipboard.js + # that gets pulled in the test in django.contrib.staticfiles.finders.FileSystemFinder + self.assertContains(response, "copy-to-clipboard", count=5) # cleanup this test domain_info.delete() diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index 9244fffcd..a903188f3 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -1526,7 +1526,9 @@ class TestDomainRequestAdmin(MockEppLib): self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) # Test for the copy link - self.assertContains(response, "copy-to-clipboard", count=5) + # We expect 5 in the form + 2 from the js module copy-to-clipboard.js + # that gets pulled in the test in django.contrib.staticfiles.finders.FileSystemFinder + self.assertContains(response, "copy-to-clipboard", count=7) # Test that Creator counts display properly self.assertNotContains(response, "Approved domains") From 9a5ada645c220c51acdb74472da8855e65ac6fb0 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 21:13:19 -0500 Subject: [PATCH 102/148] Refactor directories and filenames --- .gitignore | 2 +- src/gulpfile.js | 6 +++--- .../js/getgov-admin}/copy-to-clipboard.js | 0 .../js/getgov-admin}/domain-form.js | 0 .../js/getgov-admin}/domain-information-form.js | 0 .../js/getgov-admin}/domain-request-form.js | 0 .../js/getgov-admin}/filter-horizontal.js | 0 .../js/getgov-admin}/helpers-admin.js | 0 .../helpers-portfolio-dynamic-fields.js | 0 .../{modules-admin => src/js/getgov-admin}/main.js | 0 .../{modules-admin => src/js/getgov-admin}/modals.js | 0 .../js/getgov-admin}/portfolio-form.js | 0 .../js/getgov-admin}/show-more-description.js | 0 .../js/getgov-admin}/submit-bar.js | 0 .../assets/{modules => src/js/getgov}/combobox.js | 0 .../{modules => src/js/getgov}/domain-validators.js | 0 .../{modules => src/js/getgov}/formset-forms.js | 0 .../{modules => src/js/getgov}/helpers-csrf-token.js | 0 .../{modules => src/js/getgov}/helpers-uswds.js | 12 ++++++------ .../assets/{modules => src/js/getgov}/helpers.js | 0 .../assets/{modules => src/js/getgov}/main.js | 0 .../js/getgov}/portfolio-member-page.js | 0 .../assets/{modules => src/js/getgov}/radios.js | 0 .../{modules => src/js/getgov}/requesting-entity.js | 0 .../assets/{modules => src/js/getgov}/table-base.js | 0 .../js/getgov}/table-domain-requests.js | 0 .../{modules => src/js/getgov}/table-domains.js | 0 .../js/getgov}/table-member-domains.js | 0 .../{modules => src/js/getgov}/table-members.js | 0 .../{modules => src/js/getgov}/urbanization.js | 0 .../{modules => src/js/getgov}/user-profile.js | 0 .../assets/{ => src}/sass/_theme/_accordions.scss | 0 .../assets/{ => src}/sass/_theme/_admin.scss | 0 .../assets/{ => src}/sass/_theme/_alerts.scss | 0 .../assets/{ => src}/sass/_theme/_base.scss | 0 .../assets/{ => src}/sass/_theme/_buttons.scss | 0 .../assets/{ => src}/sass/_theme/_cisa_colors.scss | 0 .../assets/{ => src}/sass/_theme/_containers.scss | 0 .../assets/{ => src}/sass/_theme/_fieldsets.scss | 0 .../assets/{ => src}/sass/_theme/_forms.scss | 0 .../assets/{ => src}/sass/_theme/_header.scss | 0 .../assets/{ => src}/sass/_theme/_identifier.scss | 0 .../assets/{ => src}/sass/_theme/_lists.scss | 0 .../assets/{ => src}/sass/_theme/_pagination.scss | 0 .../assets/{ => src}/sass/_theme/_register-form.scss | 0 .../assets/{ => src}/sass/_theme/_search.scss | 0 .../assets/{ => src}/sass/_theme/_sidenav.scss | 0 .../assets/{ => src}/sass/_theme/_tables.scss | 0 .../assets/{ => src}/sass/_theme/_tooltips.scss | 0 .../assets/{ => src}/sass/_theme/_typography.scss | 0 .../assets/{ => src}/sass/_theme/_uswds-theme.scss | 0 .../assets/{ => src}/sass/_theme/styles.scss | 0 src/registrar/forms/domain_request_wizard.py | 2 +- src/registrar/templates/admin/base_site.html | 2 +- src/registrar/templates/base.html | 2 +- .../templates/domain_request_dotgov_domain.html | 2 +- .../templates/domain_request_org_contact.html | 2 +- src/zap.conf | 4 ++-- 58 files changed, 17 insertions(+), 17 deletions(-) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/copy-to-clipboard.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/domain-form.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/domain-information-form.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/domain-request-form.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/filter-horizontal.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/helpers-admin.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/helpers-portfolio-dynamic-fields.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/main.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/modals.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/portfolio-form.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/show-more-description.js (100%) rename src/registrar/assets/{modules-admin => src/js/getgov-admin}/submit-bar.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/combobox.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/domain-validators.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/formset-forms.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/helpers-csrf-token.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/helpers-uswds.js (76%) rename src/registrar/assets/{modules => src/js/getgov}/helpers.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/main.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/portfolio-member-page.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/radios.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/requesting-entity.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/table-base.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/table-domain-requests.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/table-domains.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/table-member-domains.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/table-members.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/urbanization.js (100%) rename src/registrar/assets/{modules => src/js/getgov}/user-profile.js (100%) rename src/registrar/assets/{ => src}/sass/_theme/_accordions.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_admin.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_alerts.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_base.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_buttons.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_cisa_colors.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_containers.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_fieldsets.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_forms.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_header.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_identifier.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_lists.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_pagination.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_register-form.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_search.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_sidenav.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_tables.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_tooltips.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_typography.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/_uswds-theme.scss (100%) rename src/registrar/assets/{ => src}/sass/_theme/styles.scss (100%) diff --git a/.gitignore b/.gitignore index d78ca3227..8acace70f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ docs/research/data/** **/assets/* -!**/assets/sass/ +!**/assets/src/ !**/assets/modules-common/ !**/assets/modules/ !**/assets/modules-admin/ diff --git a/src/gulpfile.js b/src/gulpfile.js index 31e8da8bb..333f465e8 100644 --- a/src/gulpfile.js +++ b/src/gulpfile.js @@ -8,8 +8,8 @@ const TerserPlugin = require('terser-webpack-plugin'); const ASSETS_DIR = './registrar/assets/'; const JS_BUNDLE_DEST = ASSETS_DIR + 'js'; const JS_SOURCES = [ - { src: ASSETS_DIR + 'modules/*.js', output: 'get-gov.js' }, - { src: ASSETS_DIR + 'modules-admin/*.js', output: 'get-gov-admin.js' }, + { src: ASSETS_DIR + 'src/js/getgov/*.js', output: 'getgov.min.js' }, + { src: ASSETS_DIR + 'src/js/getgov-admin/*.js', output: 'getgov-admin.min.js' }, ]; /** @@ -24,7 +24,7 @@ uswds.settings.version = 3; */ uswds.paths.dist.css = ASSETS_DIR + 'css'; uswds.paths.dist.sass = ASSETS_DIR + 'sass'; -uswds.paths.dist.theme = ASSETS_DIR + 'sass/_theme'; +uswds.paths.dist.theme = ASSETS_DIR + 'src/sass/_theme'; uswds.paths.dist.fonts = ASSETS_DIR + 'fonts'; uswds.paths.dist.js = ASSETS_DIR + 'js'; uswds.paths.dist.img = ASSETS_DIR + 'img'; diff --git a/src/registrar/assets/modules-admin/copy-to-clipboard.js b/src/registrar/assets/src/js/getgov-admin/copy-to-clipboard.js similarity index 100% rename from src/registrar/assets/modules-admin/copy-to-clipboard.js rename to src/registrar/assets/src/js/getgov-admin/copy-to-clipboard.js diff --git a/src/registrar/assets/modules-admin/domain-form.js b/src/registrar/assets/src/js/getgov-admin/domain-form.js similarity index 100% rename from src/registrar/assets/modules-admin/domain-form.js rename to src/registrar/assets/src/js/getgov-admin/domain-form.js diff --git a/src/registrar/assets/modules-admin/domain-information-form.js b/src/registrar/assets/src/js/getgov-admin/domain-information-form.js similarity index 100% rename from src/registrar/assets/modules-admin/domain-information-form.js rename to src/registrar/assets/src/js/getgov-admin/domain-information-form.js diff --git a/src/registrar/assets/modules-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js similarity index 100% rename from src/registrar/assets/modules-admin/domain-request-form.js rename to src/registrar/assets/src/js/getgov-admin/domain-request-form.js diff --git a/src/registrar/assets/modules-admin/filter-horizontal.js b/src/registrar/assets/src/js/getgov-admin/filter-horizontal.js similarity index 100% rename from src/registrar/assets/modules-admin/filter-horizontal.js rename to src/registrar/assets/src/js/getgov-admin/filter-horizontal.js diff --git a/src/registrar/assets/modules-admin/helpers-admin.js b/src/registrar/assets/src/js/getgov-admin/helpers-admin.js similarity index 100% rename from src/registrar/assets/modules-admin/helpers-admin.js rename to src/registrar/assets/src/js/getgov-admin/helpers-admin.js diff --git a/src/registrar/assets/modules-admin/helpers-portfolio-dynamic-fields.js b/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js similarity index 100% rename from src/registrar/assets/modules-admin/helpers-portfolio-dynamic-fields.js rename to src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js diff --git a/src/registrar/assets/modules-admin/main.js b/src/registrar/assets/src/js/getgov-admin/main.js similarity index 100% rename from src/registrar/assets/modules-admin/main.js rename to src/registrar/assets/src/js/getgov-admin/main.js diff --git a/src/registrar/assets/modules-admin/modals.js b/src/registrar/assets/src/js/getgov-admin/modals.js similarity index 100% rename from src/registrar/assets/modules-admin/modals.js rename to src/registrar/assets/src/js/getgov-admin/modals.js diff --git a/src/registrar/assets/modules-admin/portfolio-form.js b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js similarity index 100% rename from src/registrar/assets/modules-admin/portfolio-form.js rename to src/registrar/assets/src/js/getgov-admin/portfolio-form.js diff --git a/src/registrar/assets/modules-admin/show-more-description.js b/src/registrar/assets/src/js/getgov-admin/show-more-description.js similarity index 100% rename from src/registrar/assets/modules-admin/show-more-description.js rename to src/registrar/assets/src/js/getgov-admin/show-more-description.js diff --git a/src/registrar/assets/modules-admin/submit-bar.js b/src/registrar/assets/src/js/getgov-admin/submit-bar.js similarity index 100% rename from src/registrar/assets/modules-admin/submit-bar.js rename to src/registrar/assets/src/js/getgov-admin/submit-bar.js diff --git a/src/registrar/assets/modules/combobox.js b/src/registrar/assets/src/js/getgov/combobox.js similarity index 100% rename from src/registrar/assets/modules/combobox.js rename to src/registrar/assets/src/js/getgov/combobox.js diff --git a/src/registrar/assets/modules/domain-validators.js b/src/registrar/assets/src/js/getgov/domain-validators.js similarity index 100% rename from src/registrar/assets/modules/domain-validators.js rename to src/registrar/assets/src/js/getgov/domain-validators.js diff --git a/src/registrar/assets/modules/formset-forms.js b/src/registrar/assets/src/js/getgov/formset-forms.js similarity index 100% rename from src/registrar/assets/modules/formset-forms.js rename to src/registrar/assets/src/js/getgov/formset-forms.js diff --git a/src/registrar/assets/modules/helpers-csrf-token.js b/src/registrar/assets/src/js/getgov/helpers-csrf-token.js similarity index 100% rename from src/registrar/assets/modules/helpers-csrf-token.js rename to src/registrar/assets/src/js/getgov/helpers-csrf-token.js diff --git a/src/registrar/assets/modules/helpers-uswds.js b/src/registrar/assets/src/js/getgov/helpers-uswds.js similarity index 76% rename from src/registrar/assets/modules/helpers-uswds.js rename to src/registrar/assets/src/js/getgov/helpers-uswds.js index bb861ab9c..129d578b6 100644 --- a/src/registrar/assets/modules/helpers-uswds.js +++ b/src/registrar/assets/src/js/getgov/helpers-uswds.js @@ -1,7 +1,7 @@ /** * Initialize USWDS tooltips by calling initialization method. Requires that uswds-edited.js - * be loaded before get-gov.js. uswds-edited.js adds the tooltip module to the window to be - * accessible directly in get-gov.js + * be loaded before getgov.min.js. uswds-edited.js adds the tooltip module to the window to be + * accessible directly in getgov.min.js * */ export function initializeTooltips() { @@ -19,8 +19,8 @@ export function initializeTooltips() { /** * Initialize USWDS modals by calling on method. Requires that uswds-edited.js be loaded - * before get-gov.js. uswds-edited.js adds the modal module to the window to be accessible - * directly in get-gov.js. + * before getgov.min.js. uswds-edited.js adds the modal module to the window to be accessible + * directly in getgov.min.js. * uswdsInitializeModals adds modal-related DOM elements, based on other DOM elements existing in * the page. It needs to be called only once for any particular DOM element; otherwise, it * will initialize improperly. Therefore, if DOM elements change dynamically and include @@ -33,8 +33,8 @@ export function uswdsInitializeModals() { /** * Unload existing USWDS modals by calling off method. Requires that uswds-edited.js be - * loaded before get-gov.js. uswds-edited.js adds the modal module to the window to be - * accessible directly in get-gov.js. + * loaded before getgov.min.js. uswds-edited.js adds the modal module to the window to be + * accessible directly in getgov.min.js. * See note above with regards to calling this method relative to uswdsInitializeModals. * */ diff --git a/src/registrar/assets/modules/helpers.js b/src/registrar/assets/src/js/getgov/helpers.js similarity index 100% rename from src/registrar/assets/modules/helpers.js rename to src/registrar/assets/src/js/getgov/helpers.js diff --git a/src/registrar/assets/modules/main.js b/src/registrar/assets/src/js/getgov/main.js similarity index 100% rename from src/registrar/assets/modules/main.js rename to src/registrar/assets/src/js/getgov/main.js diff --git a/src/registrar/assets/modules/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js similarity index 100% rename from src/registrar/assets/modules/portfolio-member-page.js rename to src/registrar/assets/src/js/getgov/portfolio-member-page.js diff --git a/src/registrar/assets/modules/radios.js b/src/registrar/assets/src/js/getgov/radios.js similarity index 100% rename from src/registrar/assets/modules/radios.js rename to src/registrar/assets/src/js/getgov/radios.js diff --git a/src/registrar/assets/modules/requesting-entity.js b/src/registrar/assets/src/js/getgov/requesting-entity.js similarity index 100% rename from src/registrar/assets/modules/requesting-entity.js rename to src/registrar/assets/src/js/getgov/requesting-entity.js diff --git a/src/registrar/assets/modules/table-base.js b/src/registrar/assets/src/js/getgov/table-base.js similarity index 100% rename from src/registrar/assets/modules/table-base.js rename to src/registrar/assets/src/js/getgov/table-base.js diff --git a/src/registrar/assets/modules/table-domain-requests.js b/src/registrar/assets/src/js/getgov/table-domain-requests.js similarity index 100% rename from src/registrar/assets/modules/table-domain-requests.js rename to src/registrar/assets/src/js/getgov/table-domain-requests.js diff --git a/src/registrar/assets/modules/table-domains.js b/src/registrar/assets/src/js/getgov/table-domains.js similarity index 100% rename from src/registrar/assets/modules/table-domains.js rename to src/registrar/assets/src/js/getgov/table-domains.js diff --git a/src/registrar/assets/modules/table-member-domains.js b/src/registrar/assets/src/js/getgov/table-member-domains.js similarity index 100% rename from src/registrar/assets/modules/table-member-domains.js rename to src/registrar/assets/src/js/getgov/table-member-domains.js diff --git a/src/registrar/assets/modules/table-members.js b/src/registrar/assets/src/js/getgov/table-members.js similarity index 100% rename from src/registrar/assets/modules/table-members.js rename to src/registrar/assets/src/js/getgov/table-members.js diff --git a/src/registrar/assets/modules/urbanization.js b/src/registrar/assets/src/js/getgov/urbanization.js similarity index 100% rename from src/registrar/assets/modules/urbanization.js rename to src/registrar/assets/src/js/getgov/urbanization.js diff --git a/src/registrar/assets/modules/user-profile.js b/src/registrar/assets/src/js/getgov/user-profile.js similarity index 100% rename from src/registrar/assets/modules/user-profile.js rename to src/registrar/assets/src/js/getgov/user-profile.js diff --git a/src/registrar/assets/sass/_theme/_accordions.scss b/src/registrar/assets/src/sass/_theme/_accordions.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_accordions.scss rename to src/registrar/assets/src/sass/_theme/_accordions.scss diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_admin.scss rename to src/registrar/assets/src/sass/_theme/_admin.scss diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/src/sass/_theme/_alerts.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_alerts.scss rename to src/registrar/assets/src/sass/_theme/_alerts.scss diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/src/sass/_theme/_base.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_base.scss rename to src/registrar/assets/src/sass/_theme/_base.scss diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/src/sass/_theme/_buttons.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_buttons.scss rename to src/registrar/assets/src/sass/_theme/_buttons.scss diff --git a/src/registrar/assets/sass/_theme/_cisa_colors.scss b/src/registrar/assets/src/sass/_theme/_cisa_colors.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_cisa_colors.scss rename to src/registrar/assets/src/sass/_theme/_cisa_colors.scss diff --git a/src/registrar/assets/sass/_theme/_containers.scss b/src/registrar/assets/src/sass/_theme/_containers.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_containers.scss rename to src/registrar/assets/src/sass/_theme/_containers.scss diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/src/sass/_theme/_fieldsets.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_fieldsets.scss rename to src/registrar/assets/src/sass/_theme/_fieldsets.scss diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/src/sass/_theme/_forms.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_forms.scss rename to src/registrar/assets/src/sass/_theme/_forms.scss diff --git a/src/registrar/assets/sass/_theme/_header.scss b/src/registrar/assets/src/sass/_theme/_header.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_header.scss rename to src/registrar/assets/src/sass/_theme/_header.scss diff --git a/src/registrar/assets/sass/_theme/_identifier.scss b/src/registrar/assets/src/sass/_theme/_identifier.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_identifier.scss rename to src/registrar/assets/src/sass/_theme/_identifier.scss diff --git a/src/registrar/assets/sass/_theme/_lists.scss b/src/registrar/assets/src/sass/_theme/_lists.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_lists.scss rename to src/registrar/assets/src/sass/_theme/_lists.scss diff --git a/src/registrar/assets/sass/_theme/_pagination.scss b/src/registrar/assets/src/sass/_theme/_pagination.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_pagination.scss rename to src/registrar/assets/src/sass/_theme/_pagination.scss diff --git a/src/registrar/assets/sass/_theme/_register-form.scss b/src/registrar/assets/src/sass/_theme/_register-form.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_register-form.scss rename to src/registrar/assets/src/sass/_theme/_register-form.scss diff --git a/src/registrar/assets/sass/_theme/_search.scss b/src/registrar/assets/src/sass/_theme/_search.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_search.scss rename to src/registrar/assets/src/sass/_theme/_search.scss diff --git a/src/registrar/assets/sass/_theme/_sidenav.scss b/src/registrar/assets/src/sass/_theme/_sidenav.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_sidenav.scss rename to src/registrar/assets/src/sass/_theme/_sidenav.scss diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/src/sass/_theme/_tables.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_tables.scss rename to src/registrar/assets/src/sass/_theme/_tables.scss diff --git a/src/registrar/assets/sass/_theme/_tooltips.scss b/src/registrar/assets/src/sass/_theme/_tooltips.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_tooltips.scss rename to src/registrar/assets/src/sass/_theme/_tooltips.scss diff --git a/src/registrar/assets/sass/_theme/_typography.scss b/src/registrar/assets/src/sass/_theme/_typography.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_typography.scss rename to src/registrar/assets/src/sass/_theme/_typography.scss diff --git a/src/registrar/assets/sass/_theme/_uswds-theme.scss b/src/registrar/assets/src/sass/_theme/_uswds-theme.scss similarity index 100% rename from src/registrar/assets/sass/_theme/_uswds-theme.scss rename to src/registrar/assets/src/sass/_theme/_uswds-theme.scss diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/src/sass/_theme/styles.scss similarity index 100% rename from src/registrar/assets/sass/_theme/styles.scss rename to src/registrar/assets/src/sass/_theme/styles.scss diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index bfbc22124..ce2f27b75 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -35,7 +35,7 @@ class RequestingEntityForm(RegistrarForm): # If this selection is made on the form (tracked by js), then it will toggle the form value of this. # In other words, this essentially tracks if the suborganization field == "Other". # "Other" is just an imaginary value that is otherwise invalid. - # Note the logic in `def clean` and `handleRequestingEntityFieldset` in get-gov.js + # Note the logic in `def clean` and `handleRequestingEntityFieldset` in getgov.min.js is_requesting_new_suborganization = forms.BooleanField(required=False, widget=forms.HiddenInput()) sub_organization = forms.ModelChoiceField( diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index 5ca5edffc..b80917bb2 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -21,7 +21,7 @@ - + {% endblock %} diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index b123a0eac..bda043590 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -47,7 +47,7 @@ - + {% endblock %} {% block canonical %} diff --git a/src/registrar/templates/domain_request_dotgov_domain.html b/src/registrar/templates/domain_request_dotgov_domain.html index 18e04f305..6c62c6497 100644 --- a/src/registrar/templates/domain_request_dotgov_domain.html +++ b/src/registrar/templates/domain_request_dotgov_domain.html @@ -49,7 +49,7 @@

        After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all our requirements after you complete the rest of this form.

        {% with attr_aria_describedby="domain_instructions domain_instructions2" %} - {# attr_validate / validate="domain" invokes code in get-gov.js #} + {# attr_validate / validate="domain" invokes code in getgov.min.js #} {% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %} {% input_with_errors forms.0.requested_domain %} {% endwith %} diff --git a/src/registrar/templates/domain_request_org_contact.html b/src/registrar/templates/domain_request_org_contact.html index 44b404bbf..d39fb9f78 100644 --- a/src/registrar/templates/domain_request_org_contact.html +++ b/src/registrar/templates/domain_request_org_contact.html @@ -44,5 +44,5 @@ {% endblock %} - + diff --git a/src/zap.conf b/src/zap.conf index 1f0548f2d..7b878fb90 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -29,8 +29,8 @@ 10027 OUTOFSCOPE http://app:8080/public/js/uswds.min.js # UNCLEAR WHY THIS ONE IS FAILING. Giving 404 error. 10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js -# get-gov.js contains suspicious word "from" as in `Array.from()` -10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js +# getgov.min.js contains suspicious word "from" as in `Array.from()` +10027 OUTOFSCOPE http://app:8080/public/js/getgov.min.js # Ignores suspicious word "TODO" 10027 OUTOFSCOPE http://app:8080.*$ 10028 FAIL (Open Redirect - Passive/beta) From 74bb8edcb103c24eef79242addae8d5c68580c69 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 21:32:21 -0500 Subject: [PATCH 103/148] try to track sass folder removal in git --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8acace70f..fb23dfb2b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ docs/research/data/** **/assets/* !**/assets/src/ +!**/assets/sass/ !**/assets/modules-common/ !**/assets/modules/ !**/assets/modules-admin/ From 03d3d109dc768aac5aa9d3787a3cd014a07c63b1 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 21:34:55 -0500 Subject: [PATCH 104/148] remove junk from gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index fb23dfb2b..b49b30639 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,6 @@ docs/research/data/** **/assets/* !**/assets/src/ !**/assets/sass/ -!**/assets/modules-common/ -!**/assets/modules/ -!**/assets/modules-admin/ !**/assets/img/registrar/ public/ credentials* From ff8a4a3fa193a84f58276d790710effc633a708e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 20 Nov 2024 21:42:40 -0500 Subject: [PATCH 105/148] clean up test users --- src/registrar/fixtures/fixtures_users.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index be4afa311..a8cdb5b9a 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -23,13 +23,6 @@ class UserFixture: """ ADMINS = [ - # { - # "username": "a6b5dd22-f54e-4d55-83ce-ca7b65b2dc1a", - # "first_name": "Rach test admin", - # "last_name": "Mrad test admin", - # "email": "rachid.mrad+2@gmail.com", - # "title": "Super admin tester", - # }, { "username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf", "first_name": "Aditi", @@ -161,13 +154,6 @@ class UserFixture: ] STAFF = [ - # { - # "username": "994b7a90-f1d1-4140-a3d2-ff34183c7ee2", - # "first_name": "Rach test staff", - # "last_name": "Mrad test staff", - # "email": "rachid.mrad+3@gmail.com", - # "title": "Super staff tester", - # }, { "username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54", "first_name": "Aditi-Analyst", From 4eb54319b783e6b02ed92dd276e570379815de43 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:32:42 -0700 Subject: [PATCH 106/148] Update src/registrar/utility/csv_export.py --- src/registrar/utility/csv_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index db5eb1657..f0fb80ed5 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -70,7 +70,7 @@ def format_end_date(end_date): class BaseExport(BaseModelAnnotation): """ A generic class for exporting data which returns a csv file for the given model. - 3rd class in an inheritance tree of 4. + 2nd class in an inheritance tree of 4. """ @classmethod From 0b69a5b0c21f3d7656d031e85cab333fc82a0ec9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:20:34 -0700 Subject: [PATCH 107/148] Remove js for style --- src/registrar/assets/js/get-gov.js | 8 +------- .../templates/domain_request_requesting_entity.html | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 222c675e4..ee8ec5001 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2938,13 +2938,7 @@ document.addEventListener("DOMContentLoaded", () => { if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True"; requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer); requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False"; - if (requestingNewSuborganization.value === "True") { - selectParent.classList.add("padding-bottom-2"); - showElement(suborgDetailsContainer); - }else { - selectParent.classList.remove("padding-bottom-2"); - hideElement(suborgDetailsContainer); - } + requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer); } // Add fake "other" option to sub_organization select diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index 3dac6a974..b16e7e402 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -43,7 +43,7 @@ {% comment %} This will be toggled if a special value, "other", is selected. Otherwise this field is invisible. {% endcomment %} -
        +
        {% with attr_required=True %} {% input_with_errors forms.1.requested_suborganization %} {% endwith %} From 7e0cdb813c0ad81ff80fe0be10c0244e0f9ed73b Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 25 Nov 2024 15:20:30 -0500 Subject: [PATCH 108/148] updates --- src/registrar/assets/js/get-gov.js | 3 ++- src/registrar/templates/domain_request_requesting_entity.html | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index f29ab7f47..bfb40da0e 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2925,6 +2925,7 @@ document.addEventListener("DOMContentLoaded", () => { const selectParent = select?.parentElement; const suborgContainer = document.getElementById("suborganization-container"); const suborgDetailsContainer = document.getElementById("suborganization-container__details"); + const subOrgCreateNewOption = document.getElementById("option-to-add-suborg").value if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return; // requestingSuborganization: This just broadly determines if they're requesting a suborg at all @@ -2949,7 +2950,7 @@ document.addEventListener("DOMContentLoaded", () => { // Add fake "other" option to sub_organization select if (select && !Array.from(select.options).some(option => option.value === "other")) { - select.add(new Option("Other (enter your suborganization manually)", "other")); + select.add(new Option(subOrgCreateNewOption, "other")); } if (requestingNewSuborganization.value === "True") { diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index 3dac6a974..bcd3a45dd 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -7,6 +7,7 @@ {% endblock %} {% block form_fields %} +

        Who will use the domain you’re requesting?

        From 859e8b3bfaa16367a9b4e793475e1bc810a03a48 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:26:04 -0700 Subject: [PATCH 109/148] remove model annotations file --- src/registrar/utility/csv_export.py | 312 +++++++++++- src/registrar/utility/model_annotations.py | 447 ------------------ src/registrar/views/portfolio_members_json.py | 115 ++++- 3 files changed, 394 insertions(+), 480 deletions(-) delete mode 100644 src/registrar/utility/model_annotations.py diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index f0fb80ed5..2f255fd49 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -21,6 +21,7 @@ from django.db.models import ( Value, When, ) +from abc import ABC, abstractmethod from django.utils import timezone from django.db.models.functions import Concat, Coalesce from django.contrib.postgres.aggregates import StringAgg @@ -30,13 +31,34 @@ from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.templatetags.custom_filters import get_region from registrar.utility.constants import BranchChoices from registrar.utility.enums import DefaultEmail - -from registrar.utility.model_annotations import ( - BaseModelAnnotation, - PortfolioInvitationModelAnnotation, - UserPortfolioPermissionModelAnnotation, +from abc import ABC, abstractmethod +from registrar.models import ( + DomainInvitation, + PortfolioInvitation, ) - +from django.db.models import ( + CharField, + F, + ManyToManyField, + Q, + QuerySet, + Value, + TextField, + OuterRef, + Subquery, + Func, + Case, + When, + Exists, +) +from django.db.models.functions import Concat, Coalesce, Cast +from registrar.models.user_group import UserGroup +from registrar.models.user_portfolio_permission import UserPortfolioPermission +from registrar.models.utility.generic_helper import convert_queryset_to_dict +from registrar.models.utility.orm_helper import ArrayRemoveNull +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.admin.models import LogEntry, ADDITION +from django.contrib.contenttypes.models import ContentType logger = logging.getLogger(__name__) @@ -67,12 +89,162 @@ def format_end_date(end_date): return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date() -class BaseExport(BaseModelAnnotation): +class BaseExport(ABC): """ A generic class for exporting data which returns a csv file for the given model. 2nd class in an inheritance tree of 4. """ + @classmethod + @abstractmethod + def model(self): + """ + Property to specify the model that the export class will handle. + Must be implemented by subclasses. + """ + pass + + @classmethod + def get_sort_fields(cls): + """ + Returns the sort fields for the CSV export. Override in subclasses as needed. + """ + return [] + + @classmethod + def get_additional_args(cls): + """ + Returns additional keyword arguments as an empty dictionary. + Override in subclasses to provide specific arguments. + """ + return {} + + @classmethod + def get_select_related(cls): + """ + Get a list of tables to pass to select_related when building queryset. + """ + return [] + + @classmethod + def get_prefetch_related(cls): + """ + Get a list of tables to pass to prefetch_related when building queryset. + """ + return [] + + @classmethod + def get_exclusions(cls): + """ + Get a Q object of exclusion conditions to pass to .exclude() when building queryset. + """ + return Q() + + @classmethod + def get_filter_conditions(cls, **kwargs): + """ + Get a Q object of filter conditions to filter when building queryset. + """ + return Q() + + @classmethod + def get_annotated_fields(cls, **kwargs): + """ + Get a dict of computed fields. These are fields that do not exist on the model normally + and will be passed to .annotate() when building a queryset. + """ + return {} + + @classmethod + def get_annotations_for_sort(cls): + """ + Get a dict of annotations to make available for order_by clause. + """ + return {} + + @classmethod + def get_related_table_fields(cls): + """ + Get a list of fields from related tables. + """ + return [] + + @classmethod + def annotate_and_retrieve_fields( + cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs + ) -> QuerySet: + """ + Applies annotations to a queryset and retrieves specified fields, + including class-defined and annotation-defined. + + Parameters: + initial_queryset (QuerySet): Initial queryset. + annotated_fields (dict, optional): Fields to compute {field_name: expression}. + related_table_fields (list, optional): Extra fields to retrieve; defaults to annotation keys if None. + include_many_to_many (bool, optional): Determines if we should include many to many fields or not + **kwargs: Additional keyword arguments for specific parameters (e.g., public_contacts, domain_invitations, + user_domain_roles). + + Returns: + QuerySet: Contains dictionaries with the specified fields for each record. + """ + if related_table_fields is None: + related_table_fields = [] + + # We can infer that if we're passing in annotations, + # we want to grab the result of said annotation. + if annotated_fields: + related_table_fields.extend(annotated_fields.keys()) + + # Get prexisting fields on the model + model_fields = set() + for field in cls.model()._meta.get_fields(): + # Exclude many to many fields unless we specify + many_to_many = isinstance(field, ManyToManyField) and include_many_to_many + if many_to_many or not isinstance(field, ManyToManyField): + model_fields.add(field.name) + + queryset = initial_queryset.annotate(**annotated_fields).values(*model_fields, *related_table_fields) + + return cls.update_queryset(queryset, **kwargs) + + @classmethod + def get_annotated_queryset(cls, **kwargs): + sort_fields = cls.get_sort_fields() + # Get additional args and merge with incoming kwargs + additional_args = cls.get_additional_args() + kwargs.update(additional_args) + select_related = cls.get_select_related() + prefetch_related = cls.get_prefetch_related() + exclusions = cls.get_exclusions() + annotations_for_sort = cls.get_annotations_for_sort() + filter_conditions = cls.get_filter_conditions(**kwargs) + annotated_fields = cls.get_annotated_fields(**kwargs) + related_table_fields = cls.get_related_table_fields() + + model_queryset = ( + cls.model() + .objects.select_related(*select_related) + .prefetch_related(*prefetch_related) + .filter(filter_conditions) + .exclude(exclusions) + .annotate(**annotations_for_sort) + .order_by(*sort_fields) + .distinct() + ) + return cls.annotate_and_retrieve_fields(model_queryset, annotated_fields, related_table_fields, **kwargs) + + @classmethod + def update_queryset(cls, queryset, **kwargs): + """ + Returns an updated queryset. Override in subclass to update queryset. + """ + return queryset + + @classmethod + def get_model_annotation_dict(cls, **kwargs): + return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) + @classmethod def get_columns(cls): """ @@ -181,14 +353,128 @@ class MemberExport(BaseExport): "joined_date", "invited_by", ] - permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values( - *shared_columns - ) - invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values( - *shared_columns - ) + + # Permissions + permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio).select_related("user").annotate( + first_name=F("user__first_name"), + last_name=F("user__last_name"), + email_display=F("user__email"), + last_active=Coalesce( + Func( + F("user__last_login"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField() + ), + Value("Invalid date"), + output_field=CharField(), + ), + additional_permissions_display=F("additional_permissions"), + member_display=Case( + # If email is present and not blank, use email + When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), + # If first name or last name is present, use concatenation of first_name + " " + last_name + When( + Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), + then=Concat( + Coalesce(F("user__first_name"), Value("")), + Value(" "), + Coalesce(F("user__last_name"), Value("")), + ), + ), + # If neither, use an empty string + default=Value(""), + output_field=CharField(), + ), + domain_info=ArrayAgg( + F("user__permissions__domain__name"), + distinct=True, + # only include domains in portfolio + filter=Q(user__permissions__domain__isnull=False) + & Q(user__permissions__domain__domain_info__portfolio=portfolio), + ), + type=Value("member", output_field=CharField()), + joined_date=Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=CharField()), + invited_by=cls.get_invited_by_query( + object_id_query=cls.get_portfolio_invitation_id_query() + ), + ).values(*shared_columns) + + # Invitations + domain_invitations = DomainInvitation.objects.filter( + email=OuterRef("email"), # Check if email matches the OuterRef("email") + domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio + ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) + invitations = PortfolioInvitation.objects.filter(portfolio=portfolio).annotate( + first_name=Value(None, output_field=CharField()), + last_name=Value(None, output_field=CharField()), + email_display=F("email"), + last_active=Value("Invited", output_field=CharField()), + additional_permissions_display=F("additional_permissions"), + member_display=F("email"), + # Use ArrayRemove to return an empty list when no domain invitations are found + domain_info=ArrayRemoveNull( + ArrayAgg( + Subquery(domain_invitations.values("domain_info")), + distinct=True, + ) + ), + type=Value("invitedmember", output_field=CharField()), + joined_date=Value("Unretrieved", output_field=CharField()), + invited_by=cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=CharField())), + ).values(*shared_columns) + return convert_queryset_to_dict(permissions.union(invitations), is_model=False) + @classmethod + def get_invited_by_query(cls, object_id_query): + """Returns the user that created the given portfolio invitation. + Grabs this data from the audit log, given that a portfolio invitation object + is specified via object_id_query.""" + return Coalesce( + Subquery( + LogEntry.objects.filter( + content_type=ContentType.objects.get_for_model(PortfolioInvitation), + object_id=object_id_query, + action_flag=ADDITION, + ) + .annotate( + display_email=Case( + When( + Exists( + UserGroup.objects.filter( + name__in=["cisa_analysts_group", "full_access_group"], + user=OuterRef("user"), + ) + ), + then=Value("help@get.gov"), + ), + default=F("user__email"), + output_field=CharField(), + ) + ) + .order_by("action_time") + .values("display_email")[:1] + ), + Value("System"), + output_field=CharField(), + ) + + @classmethod + def get_portfolio_invitation_id_query(cls): + """Gets the id of the portfolio invitation that created this UserPortfolioPermission. + This makes the assumption that if an invitation is retrieved, it must have created the given + UserPortfolioPermission object.""" + return Cast( + Subquery( + PortfolioInvitation.objects.filter( + status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED, + # Double outer ref because we first go into the LogEntry query, + # then into the parent UserPortfolioPermission. + email=OuterRef(OuterRef("user__email")), + portfolio=OuterRef(OuterRef("portfolio")), + ).values("id")[:1] + ), + output_field=CharField(), + ) + @classmethod def get_columns(cls): """ diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py deleted file mode 100644 index 0fc430f4f..000000000 --- a/src/registrar/utility/model_annotations.py +++ /dev/null @@ -1,447 +0,0 @@ -""" -Model annotation classes. - -Intended to return django querysets with computed fields for api endpoints and our csv reports. -Used by both API endpoints (e.g. portfolio members JSON) and data exports (e.g. CSV reports). - -Initially created to manage the complexity of the MembersTable and Members CSV report, -as they require complex but common annotations. - -Example: - # For a JSON table endpoint - permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio) - # Returns queryset with standardized fields for member tables - - # For a CSV export - permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio, csv_report=True) - # Returns same fields but formatted for CSV export -""" - -from abc import ABC, abstractmethod -from registrar.models import ( - DomainInvitation, - PortfolioInvitation, -) -from django.db.models import ( - CharField, - F, - ManyToManyField, - Q, - QuerySet, - Value, - TextField, - OuterRef, - Subquery, - Func, - Case, - When, - Exists, -) -from django.db.models.functions import Concat, Coalesce, Cast -from registrar.models.user_group import UserGroup -from registrar.models.user_portfolio_permission import UserPortfolioPermission -from registrar.models.utility.generic_helper import convert_queryset_to_dict -from registrar.models.utility.orm_helper import ArrayRemoveNull -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.admin.models import LogEntry, ADDITION -from django.contrib.contenttypes.models import ContentType - - -class BaseModelAnnotation(ABC): - """ - Abstract base class that standardizes how models are annotated for csv exports or complex annotation queries. - For example, the Members table / csv export. - - Subclasses define model-specific annotations, filters, and field formatting while inheriting - common queryset building logic. - Intended ensure consistent data presentation across both table UI components and CSV exports. - - Base class in an inheritance tree of 4. - """ - - @classmethod - @abstractmethod - def model(self): - """ - Property to specify the model that the export class will handle. - Must be implemented by subclasses. - """ - pass - - @classmethod - def get_sort_fields(cls): - """ - Returns the sort fields for the CSV export. Override in subclasses as needed. - """ - return [] - - @classmethod - def get_additional_args(cls): - """ - Returns additional keyword arguments as an empty dictionary. - Override in subclasses to provide specific arguments. - """ - return {} - - @classmethod - def get_select_related(cls): - """ - Get a list of tables to pass to select_related when building queryset. - """ - return [] - - @classmethod - def get_prefetch_related(cls): - """ - Get a list of tables to pass to prefetch_related when building queryset. - """ - return [] - - @classmethod - def get_exclusions(cls): - """ - Get a Q object of exclusion conditions to pass to .exclude() when building queryset. - """ - return Q() - - @classmethod - def get_filter_conditions(cls, **kwargs): - """ - Get a Q object of filter conditions to filter when building queryset. - """ - return Q() - - @classmethod - def get_annotated_fields(cls, **kwargs): - """ - Get a dict of computed fields. These are fields that do not exist on the model normally - and will be passed to .annotate() when building a queryset. - """ - return {} - - @classmethod - def get_annotations_for_sort(cls): - """ - Get a dict of annotations to make available for order_by clause. - """ - return {} - - @classmethod - def get_related_table_fields(cls): - """ - Get a list of fields from related tables. - """ - return [] - - @classmethod - def annotate_and_retrieve_fields( - cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs - ) -> QuerySet: - """ - Applies annotations to a queryset and retrieves specified fields, - including class-defined and annotation-defined. - - Parameters: - initial_queryset (QuerySet): Initial queryset. - annotated_fields (dict, optional): Fields to compute {field_name: expression}. - related_table_fields (list, optional): Extra fields to retrieve; defaults to annotation keys if None. - include_many_to_many (bool, optional): Determines if we should include many to many fields or not - **kwargs: Additional keyword arguments for specific parameters (e.g., public_contacts, domain_invitations, - user_domain_roles). - - Returns: - QuerySet: Contains dictionaries with the specified fields for each record. - """ - if related_table_fields is None: - related_table_fields = [] - - # We can infer that if we're passing in annotations, - # we want to grab the result of said annotation. - if annotated_fields: - related_table_fields.extend(annotated_fields.keys()) - - # Get prexisting fields on the model - model_fields = set() - for field in cls.model()._meta.get_fields(): - # Exclude many to many fields unless we specify - many_to_many = isinstance(field, ManyToManyField) and include_many_to_many - if many_to_many or not isinstance(field, ManyToManyField): - model_fields.add(field.name) - - queryset = initial_queryset.annotate(**annotated_fields).values(*model_fields, *related_table_fields) - - return cls.update_queryset(queryset, **kwargs) - - @classmethod - def get_annotated_queryset(cls, **kwargs): - sort_fields = cls.get_sort_fields() - # Get additional args and merge with incoming kwargs - additional_args = cls.get_additional_args() - kwargs.update(additional_args) - select_related = cls.get_select_related() - prefetch_related = cls.get_prefetch_related() - exclusions = cls.get_exclusions() - annotations_for_sort = cls.get_annotations_for_sort() - filter_conditions = cls.get_filter_conditions(**kwargs) - annotated_fields = cls.get_annotated_fields(**kwargs) - related_table_fields = cls.get_related_table_fields() - - model_queryset = ( - cls.model() - .objects.select_related(*select_related) - .prefetch_related(*prefetch_related) - .filter(filter_conditions) - .exclude(exclusions) - .annotate(**annotations_for_sort) - .order_by(*sort_fields) - .distinct() - ) - return cls.annotate_and_retrieve_fields(model_queryset, annotated_fields, related_table_fields, **kwargs) - - @classmethod - def update_queryset(cls, queryset, **kwargs): - """ - Returns an updated queryset. Override in subclass to update queryset. - """ - return queryset - - @classmethod - def get_model_annotation_dict(cls, **kwargs): - return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) - - -class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation): - """ - Annotates UserPortfolioPermission querysets with computed fields for member tables. - Handles formatting of user details, permissions, and related domain information - for both UI display and CSV export. - """ - - @classmethod - def model(cls): - # Return the model class that this export handles - return UserPortfolioPermission - - @classmethod - def get_select_related(cls): - """ - Get a list of tables to pass to select_related when building queryset. - """ - return ["user"] - - @classmethod - def get_filter_conditions(cls, portfolio, **kwargs): - """ - Get a Q object of filter conditions to filter when building queryset. - """ - if not portfolio: - # Return nothing - return Q(id__in=[]) - - # Get all members on this portfolio - return Q(portfolio=portfolio) - - @classmethod - def get_portfolio_invitation_id_query(cls): - """Gets the id of the portfolio invitation that created this UserPortfolioPermission. - This makes the assumption that if an invitation is retrieved, it must have created the given - UserPortfolioPermission object.""" - return Cast( - Subquery( - PortfolioInvitation.objects.filter( - status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED, - # Double outer ref because we first go into the LogEntry query, - # then into the parent UserPortfolioPermission. - email=OuterRef(OuterRef("user__email")), - portfolio=OuterRef(OuterRef("portfolio")), - ).values("id")[:1] - ), - output_field=CharField(), - ) - - @classmethod - def get_annotated_fields(cls, portfolio, csv_report=False): - """ - Get a dict of computed fields. These are fields that do not exist on the model normally - and will be passed to .annotate() when building a queryset. - """ - if not portfolio: - # Return nothing - return {} - - # Tweak the queries slightly to only return the data we need. - # When returning data for the csv report we: - # 1. Only return the domain name for 'domain_info' rather than also add ':' seperated id - # 2. Return a formatted date for 'last_active' - # These are just optimizations that are better done in SQL as opposed to python. - if csv_report: - domain_query = F("user__permissions__domain__name") - last_active_query = Func( - F("user__last_login"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField() - ) - else: - # an array of domains, with id and name, colon separated - domain_query = Concat( - F("user__permissions__domain_id"), - Value(":"), - F("user__permissions__domain__name"), - # specify the output_field to ensure union has same column types - output_field=CharField(), - ) - last_active_query = Cast(F("user__last_login"), output_field=TextField()) - - return { - "first_name": F("user__first_name"), - "last_name": F("user__last_name"), - "email_display": F("user__email"), - "last_active": Coalesce( - last_active_query, - Value("Invalid date"), - output_field=CharField(), - ), - "additional_permissions_display": F("additional_permissions"), - "member_display": Case( - # If email is present and not blank, use email - When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), - # If first name or last name is present, use concatenation of first_name + " " + last_name - When( - Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), - then=Concat( - Coalesce(F("user__first_name"), Value("")), - Value(" "), - Coalesce(F("user__last_name"), Value("")), - ), - ), - # If neither, use an empty string - default=Value(""), - output_field=CharField(), - ), - "domain_info": ArrayAgg( - domain_query, - distinct=True, - # only include domains in portfolio - filter=Q(user__permissions__domain__isnull=False) - & Q(user__permissions__domain__domain_info__portfolio=portfolio), - ), - "type": Value("member", output_field=CharField()), - "joined_date": Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=CharField()), - "invited_by": PortfolioInvitationModelAnnotation.get_invited_by_query( - object_id_query=cls.get_portfolio_invitation_id_query() - ), - } - - @classmethod - def get_annotated_queryset(cls, portfolio, csv_report=False): - """Override of the base annotated queryset to pass in portfolio""" - return super().get_annotated_queryset(portfolio=portfolio, csv_report=csv_report) - - -class PortfolioInvitationModelAnnotation(BaseModelAnnotation): - """ - Annotates PortfolioInvitation querysets with computed fields for the member table. - Handles formatting of user details, permissions, and related domain information - for both UI display and CSV export. - """ - - @classmethod - def model(cls): - # Return the model class that this export handles - return PortfolioInvitation - - @classmethod - def get_exclusions(cls): - """ - Get a Q object of exclusion conditions to pass to .exclude() when building queryset. - """ - return Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) - - @classmethod - def get_filter_conditions(cls, portfolio, **kwargs): - """ - Get a Q object of filter conditions to filter when building queryset. - """ - if not portfolio: - # Return nothing - return Q(id__in=[]) - - # Get all members on this portfolio - return Q(portfolio=portfolio) - - @classmethod - def get_invited_by_query(cls, object_id_query): - """Returns the user that created the given portfolio invitation. - Grabs this data from the audit log, given that a portfolio invitation object - is specified via object_id_query.""" - return Coalesce( - Subquery( - LogEntry.objects.filter( - content_type=ContentType.objects.get_for_model(PortfolioInvitation), - object_id=object_id_query, - action_flag=ADDITION, - ) - .annotate( - display_email=Case( - When( - Exists( - UserGroup.objects.filter( - name__in=["cisa_analysts_group", "full_access_group"], - user=OuterRef("user"), - ) - ), - then=Value("help@get.gov"), - ), - default=F("user__email"), - output_field=CharField(), - ) - ) - .order_by("action_time") - .values("display_email")[:1] - ), - Value("System"), - output_field=CharField(), - ) - - @classmethod - def get_annotated_fields(cls, portfolio, csv_report=False): - """ - Get a dict of computed fields. These are fields that do not exist on the model normally - and will be passed to .annotate() when building a queryset. - """ - if not portfolio: - # Return nothing - return {} - - # Tweak the queries slightly to only return the data we need - if csv_report: - domain_query = F("domain__name") - else: - domain_query = Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField()) - - # Get all existing domain invitations and search on that for domains the user exists on - domain_invitations = DomainInvitation.objects.filter( - email=OuterRef("email"), # Check if email matches the OuterRef("email") - domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio - ).annotate(domain_info=domain_query) - return { - "first_name": Value(None, output_field=CharField()), - "last_name": Value(None, output_field=CharField()), - "email_display": F("email"), - "last_active": Value("Invited", output_field=CharField()), - "additional_permissions_display": F("additional_permissions"), - "member_display": F("email"), - # Use ArrayRemove to return an empty list when no domain invitations are found - "domain_info": ArrayRemoveNull( - ArrayAgg( - Subquery(domain_invitations.values("domain_info")), - distinct=True, - ) - ), - "type": Value("invitedmember", output_field=CharField()), - "joined_date": Value("Unretrieved", output_field=CharField()), - "invited_by": cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=CharField())), - } - - @classmethod - def get_annotated_queryset(cls, portfolio, csv_report=False): - """Override of the base annotated queryset to pass in portfolio""" - return super().get_annotated_queryset(portfolio=portfolio, csv_report=csv_report) diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index bf89dcd82..f02aedbfa 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -3,15 +3,16 @@ from django.core.paginator import Paginator from django.db.models import F, Q from django.urls import reverse from django.views import View - +from django.db.models.expressions import Func +from django.db.models import Value, F, CharField, TextField, Q, Case, When, OuterRef, Subquery +from django.db.models.functions import Cast, Coalesce, Concat +from django.contrib.postgres.aggregates import ArrayAgg from registrar.models import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from registrar.utility.model_annotations import ( - PortfolioInvitationModelAnnotation, - UserPortfolioPermissionModelAnnotation, -) from registrar.views.utility.mixins import PortfolioMembersPermission - +from registrar.models.domain_invitation import DomainInvitation +from registrar.models.portfolio_invitation import PortfolioInvitation +from registrar.models.user_portfolio_permission import UserPortfolioPermission class PortfolioMembersJson(PortfolioMembersPermission, View): @@ -55,25 +56,92 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): def initial_permissions_search(self, portfolio): """Perform initial search for permissions before applying any filters.""" - queryset = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio) - return queryset.values( - "id", - "first_name", - "last_name", - "email_display", - "last_active", - "roles", - "additional_permissions_display", - "member_display", - "domain_info", - "type", + permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) + permissions = ( + permissions.select_related("user") + .annotate( + first_name=F("user__first_name"), + last_name=F("user__last_name"), + email_display=F("user__email"), + last_active=Coalesce( + Cast(F("user__last_login"), output_field=TextField()), # Cast last_login to text + Value("Invalid date"), + output_field=TextField(), + ), + additional_permissions_display=F("additional_permissions"), + member_display=Case( + # If email is present and not blank, use email + When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), + # If first name or last name is present, use concatenation of first_name + " " + last_name + When( + Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), + then=Concat( + Coalesce(F("user__first_name"), Value("")), + Value(" "), + Coalesce(F("user__last_name"), Value("")), + ), + ), + # If neither, use an empty string + default=Value(""), + output_field=CharField(), + ), + domain_info=ArrayAgg( + # an array of domains, with id and name, colon separated + Concat( + F("user__permissions__domain_id"), + Value(":"), + F("user__permissions__domain__name"), + # specify the output_field to ensure union has same column types + output_field=CharField(), + ), + distinct=True, + filter=Q(user__permissions__domain__isnull=False) # filter out null values + & Q( + user__permissions__domain__domain_info__portfolio=portfolio + ), # only include domains in portfolio + ), + type=Value("member", output_field=CharField()), + ) + .values( + "id", + "first_name", + "last_name", + "email_display", + "last_active", + "roles", + "additional_permissions_display", + "member_display", + "domain_info", + "type", + ) ) + return permissions def initial_invitations_search(self, portfolio): """Perform initial invitations search and get related DomainInvitation data based on the email.""" # Get DomainInvitation query for matching email and for the portfolio - queryset = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio) - return queryset.values( + domain_invitations = DomainInvitation.objects.filter( + email=OuterRef("email"), # Check if email matches the OuterRef("email") + domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio + ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) + # PortfolioInvitation query + invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) + invitations = invitations.annotate( + first_name=Value(None, output_field=CharField()), + last_name=Value(None, output_field=CharField()), + email_display=F("email"), + last_active=Value("Invited", output_field=TextField()), + additional_permissions_display=F("additional_permissions"), + member_display=F("email"), + # Use ArrayRemove to return an empty list when no domain invitations are found + domain_info=ArrayRemove( + ArrayAgg( + Subquery(domain_invitations.values("domain_info")), + distinct=True, + ) + ), + type=Value("invitedmember", output_field=CharField()), + ).values( "id", "first_name", "last_name", @@ -85,6 +153,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): "domain_info", "type", ) + return invitations def apply_search_term(self, queryset, request): """Apply search term to the queryset.""" @@ -144,3 +213,9 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): "svg_icon": ("visibility" if view_only else "settings"), } return member_json + + +# Custom Func to use array_remove to remove null values +class ArrayRemove(Func): + function = "array_remove" + template = "%(function)s(%(expressions)s, NULL)" From e813791033eed161c5f32e78320d1484d9f0a106 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:26:56 -0700 Subject: [PATCH 110/148] Update csv_export.py --- src/registrar/utility/csv_export.py | 72 +++++++++++------------------ 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 2f255fd49..286b970ce 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -1,8 +1,31 @@ -from abc import abstractmethod -from collections import defaultdict import csv import logging +from abc import ABC, abstractmethod +from collections import defaultdict from datetime import datetime + +from django.contrib.admin.models import LogEntry, ADDITION +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.aggregates import ArrayAgg, StringAgg +from django.db.models import ( + Case, + CharField, + Count, + DateField, + F, + ManyToManyField, + Q, + QuerySet, + TextField, + Value, + When, + OuterRef, + Subquery, + Exists, +) +from django.db.models.functions import Concat, Coalesce, Cast, Func +from django.utils import timezone + from registrar.models import ( Domain, DomainInvitation, @@ -10,55 +33,16 @@ from registrar.models import ( DomainInformation, PublicContact, UserDomainRole, + PortfolioInvitation, + UserGroup, ) -from django.db.models import ( - Case, - CharField, - Count, - DateField, - F, - Q, - Value, - When, -) -from abc import ABC, abstractmethod -from django.utils import timezone -from django.db.models.functions import Concat, Coalesce -from django.contrib.postgres.aggregates import StringAgg from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.generic_helper import convert_queryset_to_dict +from registrar.models.utility.orm_helper import ArrayRemoveNull from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.templatetags.custom_filters import get_region from registrar.utility.constants import BranchChoices from registrar.utility.enums import DefaultEmail -from abc import ABC, abstractmethod -from registrar.models import ( - DomainInvitation, - PortfolioInvitation, -) -from django.db.models import ( - CharField, - F, - ManyToManyField, - Q, - QuerySet, - Value, - TextField, - OuterRef, - Subquery, - Func, - Case, - When, - Exists, -) -from django.db.models.functions import Concat, Coalesce, Cast -from registrar.models.user_group import UserGroup -from registrar.models.user_portfolio_permission import UserPortfolioPermission -from registrar.models.utility.generic_helper import convert_queryset_to_dict -from registrar.models.utility.orm_helper import ArrayRemoveNull -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.admin.models import LogEntry, ADDITION -from django.contrib.contenttypes.models import ContentType logger = logging.getLogger(__name__) From e2bd982c27d339f041101f394b3b0326bb6d6f12 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:27:34 -0700 Subject: [PATCH 111/148] Update portfolio_members_json.py --- src/registrar/views/portfolio_members_json.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index f02aedbfa..8e5ee902f 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -1,18 +1,32 @@ -from django.http import JsonResponse +from django.contrib.postgres.aggregates import ArrayAgg from django.core.paginator import Paginator -from django.db.models import F, Q +from django.db.models import ( + Case, + CharField, + F, + Q, + TextField, + Value, + When, + OuterRef, + Subquery, +) +from django.db.models.expressions import Func +from django.db.models.functions import Cast, Coalesce, Concat +from django.http import JsonResponse from django.urls import reverse from django.views import View -from django.db.models.expressions import Func -from django.db.models import Value, F, CharField, TextField, Q, Case, When, OuterRef, Subquery -from django.db.models.functions import Cast, Coalesce, Concat -from django.contrib.postgres.aggregates import ArrayAgg -from registrar.models import UserPortfolioPermission -from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices + +from registrar.models import ( + DomainInvitation, + PortfolioInvitation, + UserPortfolioPermission, +) +from registrar.models.utility.portfolio_helper import ( + UserPortfolioPermissionChoices, + UserPortfolioRoleChoices, +) from registrar.views.utility.mixins import PortfolioMembersPermission -from registrar.models.domain_invitation import DomainInvitation -from registrar.models.portfolio_invitation import PortfolioInvitation -from registrar.models.user_portfolio_permission import UserPortfolioPermission class PortfolioMembersJson(PortfolioMembersPermission, View): From 4013815a04c7ece11c68f0c571007f33c18285e2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:33:47 -0700 Subject: [PATCH 112/148] Update csv_export.py --- src/registrar/utility/csv_export.py | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 286b970ce..916242682 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -88,6 +88,13 @@ class BaseExport(ABC): """ pass + @classmethod + def get_columns(cls): + """ + Returns the columns for CSV export. Override in subclasses as needed. + """ + return [] + @classmethod def get_sort_fields(cls): """ @@ -152,6 +159,21 @@ class BaseExport(ABC): Get a list of fields from related tables. """ return [] + + @classmethod + def update_queryset(cls, queryset, **kwargs): + """ + Returns an updated queryset. Override in subclass to update queryset. + """ + return queryset + + @classmethod + def write_csv_before(cls, csv_writer, **kwargs): + """ + Write to csv file before the write_csv method. + Override in subclasses where needed. + """ + pass @classmethod def annotate_and_retrieve_fields( @@ -218,32 +240,10 @@ class BaseExport(ABC): ) return cls.annotate_and_retrieve_fields(model_queryset, annotated_fields, related_table_fields, **kwargs) - @classmethod - def update_queryset(cls, queryset, **kwargs): - """ - Returns an updated queryset. Override in subclass to update queryset. - """ - return queryset - @classmethod def get_model_annotation_dict(cls, **kwargs): return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) - @classmethod - def get_columns(cls): - """ - Returns the columns for CSV export. Override in subclasses as needed. - """ - return [] - - @classmethod - def write_csv_before(cls, csv_writer, **kwargs): - """ - Write to csv file before the write_csv method. - Override in subclasses where needed. - """ - pass - @classmethod def export_data_to_csv(cls, csv_file, **kwargs): """ From 958e010c5a744e0fceeb6a0394b9c24b4e8a4c68 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:36:43 -0700 Subject: [PATCH 113/148] Update csv_export.py --- src/registrar/utility/csv_export.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 916242682..ea56f9b27 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -214,6 +214,25 @@ class BaseExport(ABC): return cls.update_queryset(queryset, **kwargs) + @classmethod + def export_data_to_csv(cls, csv_file, **kwargs): + """ + All domain metadata: + Exports domains of all statuses plus domain managers. + """ + writer = csv.writer(csv_file) + columns = cls.get_columns() + models_dict = cls.get_model_annotation_dict(**kwargs) + + # Write to csv file before the write_csv + cls.write_csv_before(writer, **kwargs) + + # Write the csv file + rows = cls.write_csv(writer, columns, models_dict) + + # Return rows that for easier parsing and testing + return rows + @classmethod def get_annotated_queryset(cls, **kwargs): sort_fields = cls.get_sort_fields() From 2b5a5fe7ca06ee128b5022ac64435e44f0a40014 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:00:20 -0700 Subject: [PATCH 114/148] undo some past changes --- src/registrar/utility/csv_export.py | 51 +++++++++---------- src/registrar/views/portfolio_members_json.py | 42 +++++---------- 2 files changed, 37 insertions(+), 56 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index ea56f9b27..598395b54 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -1,12 +1,19 @@ -import csv -import logging from abc import ABC, abstractmethod from collections import defaultdict +import csv +import logging from datetime import datetime - -from django.contrib.admin.models import LogEntry, ADDITION -from django.contrib.contenttypes.models import ContentType -from django.contrib.postgres.aggregates import ArrayAgg, StringAgg +from registrar.models import ( + Domain, + DomainInvitation, + DomainRequest, + DomainInformation, + PublicContact, + UserDomainRole, + PortfolioInvitation, + UserGroup, + UserPortfolioPermission, +) from django.db.models import ( Case, CharField, @@ -22,21 +29,13 @@ from django.db.models import ( OuterRef, Subquery, Exists, + Func, ) -from django.db.models.functions import Concat, Coalesce, Cast, Func from django.utils import timezone - -from registrar.models import ( - Domain, - DomainInvitation, - DomainRequest, - DomainInformation, - PublicContact, - UserDomainRole, - PortfolioInvitation, - UserGroup, -) -from registrar.models.user_portfolio_permission import UserPortfolioPermission +from django.db.models.functions import Concat, Coalesce, Cast +from django.contrib.postgres.aggregates import ArrayAgg, StringAgg +from django.contrib.admin.models import LogEntry, ADDITION +from django.contrib.contenttypes.models import ContentType from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.models.utility.orm_helper import ArrayRemoveNull from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices @@ -76,7 +75,7 @@ def format_end_date(end_date): class BaseExport(ABC): """ A generic class for exporting data which returns a csv file for the given model. - 2nd class in an inheritance tree of 4. + Base class in an inheritance tree of 3. """ @classmethod @@ -139,7 +138,7 @@ class BaseExport(ABC): return Q() @classmethod - def get_annotated_fields(cls, **kwargs): + def get_computed_fields(cls, **kwargs): """ Get a dict of computed fields. These are fields that do not exist on the model normally and will be passed to .annotate() when building a queryset. @@ -244,7 +243,7 @@ class BaseExport(ABC): exclusions = cls.get_exclusions() annotations_for_sort = cls.get_annotations_for_sort() filter_conditions = cls.get_filter_conditions(**kwargs) - annotated_fields = cls.get_annotated_fields(**kwargs) + annotated_fields = cls.get_computed_fields(**kwargs) related_table_fields = cls.get_related_table_fields() model_queryset = ( @@ -783,7 +782,7 @@ class DomainDataType(DomainExport): return ["domain__permissions"] @classmethod - def get_annotated_fields(cls, delimiter=", ", **kwargs): + def get_computed_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ @@ -1000,7 +999,7 @@ class DomainDataFull(DomainExport): ) @classmethod - def get_annotated_fields(cls, delimiter=", ", **kwargs): + def get_computed_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ @@ -1095,7 +1094,7 @@ class DomainDataFederal(DomainExport): ) @classmethod - def get_annotated_fields(cls, delimiter=", ", **kwargs): + def get_computed_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ @@ -1729,7 +1728,7 @@ class DomainRequestDataFull(DomainRequestExport): ] @classmethod - def get_annotated_fields(cls, delimiter=", ", **kwargs): + def get_computed_fields(cls, delimiter=", ", **kwargs): """ Get a dict of computed fields. """ diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 8e5ee902f..232ca2e6c 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -1,32 +1,19 @@ -from django.contrib.postgres.aggregates import ArrayAgg +from django.http import JsonResponse from django.core.paginator import Paginator -from django.db.models import ( - Case, - CharField, - F, - Q, - TextField, - Value, - When, - OuterRef, - Subquery, -) +from django.db.models import Value, F, CharField, TextField, Q, Case, When, OuterRef, Subquery from django.db.models.expressions import Func from django.db.models.functions import Cast, Coalesce, Concat -from django.http import JsonResponse +from django.contrib.postgres.aggregates import ArrayAgg from django.urls import reverse from django.views import View -from registrar.models import ( - DomainInvitation, - PortfolioInvitation, - UserPortfolioPermission, -) -from registrar.models.utility.portfolio_helper import ( - UserPortfolioPermissionChoices, - UserPortfolioRoleChoices, -) +from registrar.models.domain_invitation import DomainInvitation +from registrar.models.portfolio_invitation import PortfolioInvitation +from registrar.models.user_portfolio_permission import UserPortfolioPermission +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.views.utility.mixins import PortfolioMembersPermission +from registrar.models.utility.orm_helper import ArrayRemoveNull + class PortfolioMembersJson(PortfolioMembersPermission, View): @@ -53,7 +40,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): page_number = request.GET.get("page", 1) page_obj = paginator.get_page(page_number) - members = [self.serialize_members(portfolio, item, request.user) for item in page_obj.object_list] + members = [self.serialize_members(request, portfolio, item, request.user) for item in page_obj.object_list] return JsonResponse( { @@ -148,7 +135,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): additional_permissions_display=F("additional_permissions"), member_display=F("email"), # Use ArrayRemove to return an empty list when no domain invitations are found - domain_info=ArrayRemove( + domain_info=ArrayRemoveNull( ArrayAgg( Subquery(domain_invitations.values("domain_info")), distinct=True, @@ -193,7 +180,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): queryset = queryset.order_by(sort_by) return queryset - def serialize_members(self, portfolio, item, user): + def serialize_members(self, request, portfolio, item, user): # Check if the user can edit other users user_can_edit_other_users = any( user.has_perm(perm) for perm in ["registrar.full_access_permission", "registrar.change_user"] @@ -228,8 +215,3 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): } return member_json - -# Custom Func to use array_remove to remove null values -class ArrayRemove(Func): - function = "array_remove" - template = "%(function)s(%(expressions)s, NULL)" From ae975ed9f8812ecb27fac7d87ca1c6d96f1110f9 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 25 Nov 2024 19:46:53 -0700 Subject: [PATCH 115/148] Updated all error messages to have error icon --- .../templates/includes/input_with_errors.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index d1e53968e..98328612c 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -52,9 +52,14 @@ error messages, if necessary. {% if field.errors %}
        {% for error in field.errors %} - - {{ error }} - + {% endfor %}
        {% endif %} From d354f04bf156bc26141be4e6ea4ea7d5b80577be Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:48:02 -0700 Subject: [PATCH 116/148] Update src/registrar/utility/csv_export.py Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com> --- src/registrar/utility/csv_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index f0fb80ed5..f2caeff1c 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -192,7 +192,7 @@ class MemberExport(BaseExport): @classmethod def get_columns(cls): """ - Returns the columns for CSV export. Override in subclasses as needed. + Returns the list of column string names for CSV export. Override in subclasses as needed. """ return [ "Email", From 3b091b0dc3ce85bc41640c9b268e9df870dc8326 Mon Sep 17 00:00:00 2001 From: Rachid Mrad <107004823+rachidatecs@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:29:07 -0500 Subject: [PATCH 117/148] Update src/registrar/assets/src/js/getgov/main.js Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/assets/src/js/getgov/main.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js index 4774b4b41..2e1e9c4d1 100644 --- a/src/registrar/assets/src/js/getgov/main.js +++ b/src/registrar/assets/src/js/getgov/main.js @@ -17,8 +17,8 @@ initFormsetsForms(); triggerModalOnDsDataForm(); nameserversFormListener(); -hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees') -hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null) +hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees'); +hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null); hookupRadioTogglerListener( 'member_access_level', { @@ -26,8 +26,7 @@ hookupRadioTogglerListener( 'basic': 'new-member-basic-permissions' } ); -hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null) - +hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null); initializeUrbanizationToggle(); userProfileListener(); From e1ff54d2643bdd9e9e8be6da5eb17c7415de4aae Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 26 Nov 2024 11:54:11 -0500 Subject: [PATCH 118/148] revise while True --- src/registrar/fixtures/fixtures_requests.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index c413f4b62..93167ec61 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -103,11 +103,13 @@ class DomainRequestFixture: } @classmethod - def fake_dot_gov(cls): - while True: + def fake_dot_gov(cls, max_attempts=100): + """Generate a unique .gov domain name without using an infinite loop.""" + for _ in range(max_attempts): fake_name = f"{fake.slug()}.gov" if not Domain.objects.filter(name=fake_name).exists(): return DraftDomain.objects.create(name=fake_name) + raise RuntimeError(f"Failed to generate a unique .gov domain after {max_attempts} attempts") @classmethod def fake_expiration_date(cls): @@ -240,7 +242,7 @@ class DomainRequestFixture: # Filter Suborganizations by the request's portfolio portfolio_suborganizations = Suborganization.objects.filter(portfolio=request.portfolio) - # Assuming SuborganizationFixture.SUBORGS is a list of dictionaries with a "name" key + # Select a suborg that's defined in the fixtures suborganization_names = [suborg["name"] for suborg in SuborganizationFixture.SUBORGS] # Further filter by names in suborganization_names From 67a8cf425d7d1595c0bc50988d13b49e873ffb6a Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 26 Nov 2024 12:07:07 -0500 Subject: [PATCH 119/148] Add missing getCsrfToken import --- .../assets/src/js/getgov-admin/domain-request-form.js | 2 +- .../assets/src/js/getgov-admin/portfolio-form.js | 2 +- src/registrar/assets/src/js/getgov/combobox.js | 2 -- src/registrar/assets/src/js/getgov/domain-validators.js | 2 -- src/registrar/assets/src/js/getgov/helpers-csrf-token.js | 8 -------- src/registrar/assets/src/js/getgov/helpers.js | 8 ++++++++ .../assets/src/js/getgov/table-domain-requests.js | 3 +-- src/registrar/assets/src/js/getgov/table-members.js | 2 +- 8 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 src/registrar/assets/src/js/getgov/helpers-csrf-token.js diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js index 8323fc30b..596dbbb77 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js @@ -153,7 +153,7 @@ export function initApprovedDomain() { } /** - * A function for copy summary button (appears in DomainRegistry models) + * A function for copy summary button */ export function initCopyRequestSummary() { const copyButton = document.getElementById('id-copy-to-clipboard-summary'); diff --git a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js index 00781f23a..f001bf39b 100644 --- a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js +++ b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js @@ -2,7 +2,7 @@ import { hideElement, showElement } from './helpers-admin.js'; /** * A function for dynamically changing some fields on the portfolio admin model - * IMPORTANT NOTE: The logic in this function is paired handlePortfolioSelection and shoul be refactored + * IMPORTANT NOTE: The logic in this function is paired handlePortfolioSelection and should be refactored once we solidify our requirements. */ export function initDynamicPortfolioFields(){ diff --git a/src/registrar/assets/src/js/getgov/combobox.js b/src/registrar/assets/src/js/getgov/combobox.js index 139106c59..36b7aa0ad 100644 --- a/src/registrar/assets/src/js/getgov/combobox.js +++ b/src/registrar/assets/src/js/getgov/combobox.js @@ -27,9 +27,7 @@ export function loadInitialValuesForComboBoxes() { // Override the default clear button behavior such that it no longer clears the input, // it just resets to the data-initial-value. - // Due to the nature of how uswds works, this is slightly hacky. - // Use a MutationObserver to watch for changes in the dropdown list const dropdownList = comboBox.querySelector(`#${input.id}--list`); const observer = new MutationObserver(function(mutations) { diff --git a/src/registrar/assets/src/js/getgov/domain-validators.js b/src/registrar/assets/src/js/getgov/domain-validators.js index 368a1504c..4bda7b64b 100644 --- a/src/registrar/assets/src/js/getgov/domain-validators.js +++ b/src/registrar/assets/src/js/getgov/domain-validators.js @@ -47,8 +47,6 @@ function fetchJSON(endpoint, callback, url="/api/v1/") { if (xhr.status != 200) return; callback(JSON.parse(xhr.response)); }; - // nothing, don't care - // xhr.onerror = function() { }; } /** Modifies CSS and HTML when an input is valid/invalid. */ diff --git a/src/registrar/assets/src/js/getgov/helpers-csrf-token.js b/src/registrar/assets/src/js/getgov/helpers-csrf-token.js deleted file mode 100644 index a1a7a9fbd..000000000 --- a/src/registrar/assets/src/js/getgov/helpers-csrf-token.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Helper function to get the CSRF token from the cookie - * -*/ -export function getCsrfToken() { - return document.querySelector('input[name="csrfmiddlewaretoken"]').value; -} - \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/helpers.js b/src/registrar/assets/src/js/getgov/helpers.js index ea8fe5661..1afd84520 100644 --- a/src/registrar/assets/src/js/getgov/helpers.js +++ b/src/registrar/assets/src/js/getgov/helpers.js @@ -67,3 +67,11 @@ export function debounce(handler, cooldown=600) { timeout = setTimeout(() => handler.apply(context, args), cooldown); } } + +/** + * Helper function to get the CSRF token from the cookie + * +*/ +export function getCsrfToken() { + return document.querySelector('input[name="csrfmiddlewaretoken"]').value; +} diff --git a/src/registrar/assets/src/js/getgov/table-domain-requests.js b/src/registrar/assets/src/js/getgov/table-domain-requests.js index 5d5265b88..c005ed891 100644 --- a/src/registrar/assets/src/js/getgov/table-domain-requests.js +++ b/src/registrar/assets/src/js/getgov/table-domain-requests.js @@ -1,6 +1,5 @@ -import { hideElement, showElement } from './helpers.js'; +import { hideElement, showElement, getCsrfToken } from './helpers.js'; import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js'; -import { getCsrfToken } from './helpers-csrf-token.js'; import { BaseTable, addModal, generateKebabHTML } from './table-base.js'; diff --git a/src/registrar/assets/src/js/getgov/table-members.js b/src/registrar/assets/src/js/getgov/table-members.js index 25fc3de74..dfa1ed6b5 100644 --- a/src/registrar/assets/src/js/getgov/table-members.js +++ b/src/registrar/assets/src/js/getgov/table-members.js @@ -1,4 +1,4 @@ -import { hideElement, showElement } from './helpers.js'; +import { hideElement, showElement, getCsrfToken } from './helpers.js'; import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js'; import { BaseTable, addModal, generateKebabHTML } from './table-base.js'; From 2f93ad935646d090ca50986d2f0d1175ef88ba97 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:15:25 -0700 Subject: [PATCH 120/148] enums and the like --- .../models/user_portfolio_permission.py | 21 ++++++++------- .../models/utility/portfolio_helper.py | 27 +++++++++++++++++++ src/registrar/utility/csv_export.py | 6 ++++- src/registrar/utility/enums.py | 12 +++++++++ src/registrar/utility/model_annotations.py | 6 +++-- src/registrar/views/portfolio_members_json.py | 11 ++++---- 6 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index cd48b1b71..51f3fa3fe 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -2,7 +2,7 @@ from django.db import models from django.forms import ValidationError from registrar.models.user_domain_role import UserDomainRole from registrar.utility.waffle import flag_is_active_for_user -from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices, DomainRequestPermissionDisplay, MemberPermissionDisplay from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField @@ -115,26 +115,27 @@ class UserPortfolioPermission(TimeStampedModel): UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS, ] + if all(perm in all_permissions for perm in all_domain_perms): - return "Viewer Requester" + return DomainRequestPermissionDisplay.VIEWER_REQUESTER elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions: - return "Viewer" + return DomainRequestPermissionDisplay.VIEWER else: - return "None" + return DomainRequestPermissionDisplay.NONE @classmethod def get_member_permission_display(cls, roles, additional_permissions): """Class method to return a readable string for member permissions""" - # Tracks if they can view, create requests, or not do anything + # Tracks if they can view, create requests, or not do anything. + # This is different than get_domain_request_permission_display because member tracks + # permissions slightly differently. all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions) - # Note for reviewers: the reason why this isn't checking on "all" is because - # the way perms work for members is different than requests. We need to consolidate this. if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions: - return "Manager" + return MemberPermissionDisplay.MANAGER elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions: - return "Viewer" + return MemberPermissionDisplay.VIEWER else: - return "None" + return MemberPermissionDisplay.NONE def clean(self): """Extends clean method to perform additional validation, which can raise errors in django admin.""" diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index d998d7ffa..cdbda798b 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -1,3 +1,4 @@ +from enum import Enum from django.db import models @@ -40,3 +41,29 @@ class UserPortfolioPermissionChoices(models.TextChoices): @classmethod def to_dict(cls): return {key: value.value for key, value in cls.__members__.items()} + + +class DomainRequestPermissionDisplay(Enum): + """Stores display values for domain request permission combinations. + + Overview of values: + - VIEWER_REQUESTER: "Viewer Requester" + - VIEWER: "VIEWER" + - NONE: "None" + """ + VIEWER_REQUESTER = "Viewer Requester" + VIEWER = "VIEWER" + NONE = "None" + + +class MemberPermissionDisplay(Enum): + """Stores display values for member permission combinations. + + Overview of values: + - MANAGER: "Manager" + - VIEWER: "Viewer" + - NONE: "None" + """ + MANAGER = "Manager" + VIEWER = "Viewer" + NONE = "None" diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index f2caeff1c..268cb7ec8 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -160,7 +160,11 @@ class MemberExport(BaseExport): @classmethod def get_model_annotation_dict(cls, request=None, **kwargs): """Combines the permissions and invitation model annotations for - the final returned csv export which combines both of these contexts""" + the final returned csv export which combines both of these contexts. + Returns a dictionary of a union between: + - UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True) + - PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True) + """ portfolio = request.session.get("portfolio") if not portfolio: return {} diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index e430a4881..137680a23 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -35,10 +35,22 @@ class DefaultEmail(Enum): Overview of emails: - PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov" - LEGACY_DEFAULT: "registrar@dotgov.gov" + - HELP_EMAIL: "help@get.gov" """ PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov" LEGACY_DEFAULT = "registrar@dotgov.gov" + HELP_EMAIL = "help@get.gov" + + +class DefaultUser(Enum): + """Stores default values for a default user. + + Overview of defaults: + - SYSTEM: "System" + """ + + SYSTEM = "System" class Step(StrEnum): diff --git a/src/registrar/utility/model_annotations.py b/src/registrar/utility/model_annotations.py index 0fc430f4f..a90354bda 100644 --- a/src/registrar/utility/model_annotations.py +++ b/src/registrar/utility/model_annotations.py @@ -46,6 +46,8 @@ from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.admin.models import LogEntry, ADDITION from django.contrib.contenttypes.models import ContentType +from registrar.utility.enums import DefaultEmail, DefaultUser + class BaseModelAnnotation(ABC): """ @@ -388,7 +390,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): user=OuterRef("user"), ) ), - then=Value("help@get.gov"), + then=Value(DefaultEmail.HELP_EMAIL), ), default=F("user__email"), output_field=CharField(), @@ -397,7 +399,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation): .order_by("action_time") .values("display_email")[:1] ), - Value("System"), + Value(DefaultUser.SYSTEM), output_field=CharField(), ) diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index bf89dcd82..29af2b4ad 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -55,8 +55,9 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): def initial_permissions_search(self, portfolio): """Perform initial search for permissions before applying any filters.""" - queryset = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio) - return queryset.values( + # Get UserPortfolioPermission query for getting active members on the portfolio + permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio) + return permissions.values( "id", "first_name", "last_name", @@ -71,9 +72,9 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): def initial_invitations_search(self, portfolio): """Perform initial invitations search and get related DomainInvitation data based on the email.""" - # Get DomainInvitation query for matching email and for the portfolio - queryset = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio) - return queryset.values( + # Get PortfolioInvitation query for getting active invitations on the portfolio + invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio) + return invitations.values( "id", "first_name", "last_name", From e91526e602bceca2bef38542d24eec0fe1e5eb81 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:20:25 -0700 Subject: [PATCH 121/148] cleanup --- src/registrar/utility/csv_export.py | 8 ++++---- src/registrar/utility/enums.py | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 4cbd026d8..532edd759 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -41,7 +41,7 @@ from registrar.models.utility.orm_helper import ArrayRemoveNull from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.templatetags.custom_filters import get_region from registrar.utility.constants import BranchChoices -from registrar.utility.enums import DefaultEmail +from registrar.utility.enums import DefaultEmail, DefaultUserValues logger = logging.getLogger(__name__) @@ -423,7 +423,7 @@ class MemberExport(BaseExport): ) ), type=Value("invitedmember", output_field=CharField()), - joined_date=Value("Unretrieved", output_field=CharField()), + joined_date=Value(DefaultUserValues.UNRETRIEVED, output_field=CharField()), invited_by=cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=CharField())), ).values(*shared_columns) @@ -450,7 +450,7 @@ class MemberExport(BaseExport): user=OuterRef("user"), ) ), - then=Value("help@get.gov"), + then=Value(DefaultEmail.HELP_EMAIL), ), default=F("user__email"), output_field=CharField(), @@ -459,7 +459,7 @@ class MemberExport(BaseExport): .order_by("action_time") .values("display_email")[:1] ), - Value("System"), + Value(DefaultUserValues.SYSTEM), output_field=CharField(), ) diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 137680a23..fb3e20577 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -43,14 +43,17 @@ class DefaultEmail(Enum): HELP_EMAIL = "help@get.gov" -class DefaultUser(Enum): +class DefaultUserValues(Enum): """Stores default values for a default user. Overview of defaults: - - SYSTEM: "System" + - SYSTEM: "System" <= Default username + - UNRETRIEVED: "Unretrieved" <= Default email state """ SYSTEM = "System" + UNRETRIEVED = "Unretrieved" + class Step(StrEnum): From 4df17d7de04be5bbf731994da9f72294701c4089 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 26 Nov 2024 12:26:28 -0500 Subject: [PATCH 122/148] Fix nearestSpan not defined bug --- src/registrar/assets/src/js/getgov-admin/domain-request-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js index 596dbbb77..a22ea7d8c 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js @@ -292,7 +292,7 @@ export function initCopyRequestSummary() { buttonIcon.setAttribute('xlink:href', baseHref + '#check'); // Change the button text - nearestSpan = copyButton.querySelector("span") + let nearestSpan = copyButton.querySelector("span") original_text = nearestSpan.innerText nearestSpan.innerText = "Copied to clipboard" From 5ddffe18e2a0115fd5ef8475179f103d5b777787 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 26 Nov 2024 12:35:10 -0500 Subject: [PATCH 123/148] declare original_text --- .../assets/src/js/getgov-admin/domain-request-form.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js index a22ea7d8c..4621c5ac5 100644 --- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js @@ -292,14 +292,14 @@ export function initCopyRequestSummary() { buttonIcon.setAttribute('xlink:href', baseHref + '#check'); // Change the button text - let nearestSpan = copyButton.querySelector("span") - original_text = nearestSpan.innerText - nearestSpan.innerText = "Copied to clipboard" + let nearestSpan = copyButton.querySelector("span"); + let original_text = nearestSpan.innerText; + nearestSpan.innerText = "Copied to clipboard"; setTimeout(function() { // Change back to the copy icon buttonIcon.setAttribute('xlink:href', currentHref); - nearestSpan.innerText = original_text + nearestSpan.innerText = original_text; }, 2000); } From 62f0205b4ccaffed6faebe484181aa06d618b76b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:41:53 -0700 Subject: [PATCH 124/148] Fix merge conflict + enum --- .../models/utility/portfolio_helper.py | 10 +- src/registrar/tests/test_reports.py | 2 +- src/registrar/utility/csv_export.py | 130 +++++++++--------- src/registrar/utility/enums.py | 5 +- 4 files changed, 76 insertions(+), 71 deletions(-) diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index cdbda798b..9b661b316 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -1,4 +1,4 @@ -from enum import Enum +from registrar.utility import StrEnum from django.db import models @@ -43,20 +43,20 @@ class UserPortfolioPermissionChoices(models.TextChoices): return {key: value.value for key, value in cls.__members__.items()} -class DomainRequestPermissionDisplay(Enum): +class DomainRequestPermissionDisplay(StrEnum): """Stores display values for domain request permission combinations. Overview of values: - VIEWER_REQUESTER: "Viewer Requester" - - VIEWER: "VIEWER" + - VIEWER: "Viewer" - NONE: "None" """ VIEWER_REQUESTER = "Viewer Requester" - VIEWER = "VIEWER" + VIEWER = "Viewer" NONE = "None" -class MemberPermissionDisplay(Enum): +class MemberPermissionDisplay(StrEnum): """Stores display values for member permission combinations. Overview of values: diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 1289c2467..8265e3563 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -862,7 +862,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib): # Create a request and add the user to the request request = self.factory.get("/") request.user = self.user - + self.maxDiff = None # Add portfolio to session request = GenericTestHelper._mock_user_request_for_factory(request) request.session["portfolio"] = self.portfolio_1 diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 532edd759..f3315e4a0 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -158,14 +158,14 @@ class BaseExport(ABC): Get a list of fields from related tables. """ return [] - + @classmethod def update_queryset(cls, queryset, **kwargs): """ Returns an updated queryset. Override in subclass to update queryset. """ return queryset - + @classmethod def write_csv_before(cls, csv_writer, **kwargs): """ @@ -335,7 +335,7 @@ class MemberExport(BaseExport): def get_model_annotation_dict(cls, request=None, **kwargs): """Combines the permissions and invitation model annotations for the final returned csv export which combines both of these contexts. - Returns a dictionary of a union between: + Returns a dictionary of a union between: - UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True) - PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True) """ @@ -361,71 +361,77 @@ class MemberExport(BaseExport): ] # Permissions - permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio).select_related("user").annotate( - first_name=F("user__first_name"), - last_name=F("user__last_name"), - email_display=F("user__email"), - last_active=Coalesce( - Func( - F("user__last_login"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField() + permissions = ( + UserPortfolioPermission.objects.filter(portfolio=portfolio) + .select_related("user") + .annotate( + first_name=F("user__first_name"), + last_name=F("user__last_name"), + email_display=F("user__email"), + last_active=Coalesce( + Func(F("user__last_login"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()), + Value("Invalid date"), + output_field=CharField(), ), - Value("Invalid date"), - output_field=CharField(), - ), - additional_permissions_display=F("additional_permissions"), - member_display=Case( - # If email is present and not blank, use email - When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), - # If first name or last name is present, use concatenation of first_name + " " + last_name - When( - Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), - then=Concat( - Coalesce(F("user__first_name"), Value("")), - Value(" "), - Coalesce(F("user__last_name"), Value("")), + additional_permissions_display=F("additional_permissions"), + member_display=Case( + # If email is present and not blank, use email + When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), + # If first name or last name is present, use concatenation of first_name + " " + last_name + When( + Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), + then=Concat( + Coalesce(F("user__first_name"), Value("")), + Value(" "), + Coalesce(F("user__last_name"), Value("")), + ), ), + # If neither, use an empty string + default=Value(""), + output_field=CharField(), ), - # If neither, use an empty string - default=Value(""), - output_field=CharField(), - ), - domain_info=ArrayAgg( - F("user__permissions__domain__name"), - distinct=True, - # only include domains in portfolio - filter=Q(user__permissions__domain__isnull=False) - & Q(user__permissions__domain__domain_info__portfolio=portfolio), - ), - type=Value("member", output_field=CharField()), - joined_date=Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=CharField()), - invited_by=cls.get_invited_by_query( - object_id_query=cls.get_portfolio_invitation_id_query() - ), - ).values(*shared_columns) + domain_info=ArrayAgg( + F("user__permissions__domain__name"), + distinct=True, + # only include domains in portfolio + filter=Q(user__permissions__domain__isnull=False) + & Q(user__permissions__domain__domain_info__portfolio=portfolio), + ), + type=Value("member", output_field=CharField()), + joined_date=Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=CharField()), + invited_by=cls.get_invited_by_query(object_id_query=cls.get_portfolio_invitation_id_query()), + ) + .values(*shared_columns) + ) # Invitations domain_invitations = DomainInvitation.objects.filter( email=OuterRef("email"), # Check if email matches the OuterRef("email") domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio - ).annotate(domain_info=Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())) - invitations = PortfolioInvitation.objects.filter(portfolio=portfolio).annotate( - first_name=Value(None, output_field=CharField()), - last_name=Value(None, output_field=CharField()), - email_display=F("email"), - last_active=Value("Invited", output_field=CharField()), - additional_permissions_display=F("additional_permissions"), - member_display=F("email"), - # Use ArrayRemove to return an empty list when no domain invitations are found - domain_info=ArrayRemoveNull( - ArrayAgg( - Subquery(domain_invitations.values("domain_info")), - distinct=True, - ) - ), - type=Value("invitedmember", output_field=CharField()), - joined_date=Value(DefaultUserValues.UNRETRIEVED, output_field=CharField()), - invited_by=cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=CharField())), - ).values(*shared_columns) + ).annotate(domain_info=F("domain__name")) + invitations = ( + PortfolioInvitation.objects.exclude(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) + .filter(portfolio=portfolio) + .annotate( + first_name=Value(None, output_field=CharField()), + last_name=Value(None, output_field=CharField()), + email_display=F("email"), + last_active=Value("Invited", output_field=CharField()), + additional_permissions_display=F("additional_permissions"), + member_display=F("email"), + # Use ArrayRemove to return an empty list when no domain invitations are found + domain_info=ArrayRemoveNull( + ArrayAgg( + Subquery(domain_invitations.values("domain_info")), + distinct=True, + ) + ), + type=Value("invitedmember", output_field=CharField()), + joined_date=Value("Unretrieved", output_field=CharField()), + invited_by=cls.get_invited_by_query(object_id_query=Cast(OuterRef("id"), output_field=CharField())), + ) + .values(*shared_columns) + ) return convert_queryset_to_dict(permissions.union(invitations), is_model=False) @@ -450,7 +456,7 @@ class MemberExport(BaseExport): user=OuterRef("user"), ) ), - then=Value(DefaultEmail.HELP_EMAIL), + then=Value(DefaultEmail.HELP_EMAIL.value), ), default=F("user__email"), output_field=CharField(), @@ -459,7 +465,7 @@ class MemberExport(BaseExport): .order_by("action_time") .values("display_email")[:1] ), - Value(DefaultUserValues.SYSTEM), + Value(DefaultUserValues.SYSTEM.value), output_field=CharField(), ) diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index fb3e20577..14e1e87ee 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -29,7 +29,7 @@ class LogCode(Enum): DEFAULT = 5 -class DefaultEmail(Enum): +class DefaultEmail(StrEnum): """Stores the string values of default emails Overview of emails: @@ -43,7 +43,7 @@ class DefaultEmail(Enum): HELP_EMAIL = "help@get.gov" -class DefaultUserValues(Enum): +class DefaultUserValues(StrEnum): """Stores default values for a default user. Overview of defaults: @@ -55,7 +55,6 @@ class DefaultUserValues(Enum): UNRETRIEVED = "Unretrieved" - class Step(StrEnum): """ Names for each page of the domain request wizard. From c4a27489cc61805745bbce8c956f29aceb3ade44 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:45:59 -0700 Subject: [PATCH 125/148] more cleanup --- src/registrar/utility/csv_export.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index f3315e4a0..d7dc68bb9 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -176,7 +176,7 @@ class BaseExport(ABC): @classmethod def annotate_and_retrieve_fields( - cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs + cls, initial_queryset, computed_fields, related_table_fields=None, include_many_to_many=False, **kwargs ) -> QuerySet: """ Applies annotations to a queryset and retrieves specified fields, @@ -184,7 +184,7 @@ class BaseExport(ABC): Parameters: initial_queryset (QuerySet): Initial queryset. - annotated_fields (dict, optional): Fields to compute {field_name: expression}. + computed_fields (dict, optional): Fields to compute {field_name: expression}. related_table_fields (list, optional): Extra fields to retrieve; defaults to annotation keys if None. include_many_to_many (bool, optional): Determines if we should include many to many fields or not **kwargs: Additional keyword arguments for specific parameters (e.g., public_contacts, domain_invitations, @@ -198,8 +198,8 @@ class BaseExport(ABC): # We can infer that if we're passing in annotations, # we want to grab the result of said annotation. - if annotated_fields: - related_table_fields.extend(annotated_fields.keys()) + if computed_fields : + related_table_fields.extend(computed_fields .keys()) # Get prexisting fields on the model model_fields = set() @@ -209,7 +209,7 @@ class BaseExport(ABC): if many_to_many or not isinstance(field, ManyToManyField): model_fields.add(field.name) - queryset = initial_queryset.annotate(**annotated_fields).values(*model_fields, *related_table_fields) + queryset = initial_queryset.annotate(**computed_fields).values(*model_fields, *related_table_fields) return cls.update_queryset(queryset, **kwargs) @@ -234,6 +234,7 @@ class BaseExport(ABC): @classmethod def get_annotated_queryset(cls, **kwargs): + """Returns an annotated queryset based off of all query conditions.""" sort_fields = cls.get_sort_fields() # Get additional args and merge with incoming kwargs additional_args = cls.get_additional_args() @@ -243,7 +244,7 @@ class BaseExport(ABC): exclusions = cls.get_exclusions() annotations_for_sort = cls.get_annotations_for_sort() filter_conditions = cls.get_filter_conditions(**kwargs) - annotated_fields = cls.get_computed_fields(**kwargs) + computed_fields = cls.get_computed_fields(**kwargs) related_table_fields = cls.get_related_table_fields() model_queryset = ( @@ -256,7 +257,7 @@ class BaseExport(ABC): .order_by(*sort_fields) .distinct() ) - return cls.annotate_and_retrieve_fields(model_queryset, annotated_fields, related_table_fields, **kwargs) + return cls.annotate_and_retrieve_fields(model_queryset, computed_fields, related_table_fields, **kwargs) @classmethod def get_model_annotation_dict(cls, **kwargs): From bdf46b5e3f2420d25b27e2240d8401d950d1121a Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 26 Nov 2024 12:46:31 -0500 Subject: [PATCH 126/148] 2594 JS --- src/registrar/assets/src/js/getgov/requesting-entity.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/requesting-entity.js b/src/registrar/assets/src/js/getgov/requesting-entity.js index 8714a7290..2b6b30a96 100644 --- a/src/registrar/assets/src/js/getgov/requesting-entity.js +++ b/src/registrar/assets/src/js/getgov/requesting-entity.js @@ -10,9 +10,13 @@ export function handleRequestingEntityFieldset() { const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`); const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`); const select = document.getElementById(`id_${formPrefix}-sub_organization`); + const selectParent = select?.parentElement; const suborgContainer = document.getElementById("suborganization-container"); const suborgDetailsContainer = document.getElementById("suborganization-container__details"); - if (!radios || !select || !suborgContainer || !suborgDetailsContainer) return; + const subOrgCreateNewOption = document.getElementById("option-to-add-suborg").value + // Make sure all crucial page elements exist before proceeding. + // This more or less ensures that we are on the Requesting Entity page, and not elsewhere. + if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return; // requestingSuborganization: This just broadly determines if they're requesting a suborg at all // requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not. @@ -28,7 +32,7 @@ export function handleRequestingEntityFieldset() { // Add fake "other" option to sub_organization select if (select && !Array.from(select.options).some(option => option.value === "other")) { - select.add(new Option("Other (enter your organization manually)", "other")); + select.add(new Option(subOrgCreateNewOption, "other")); } if (requestingNewSuborganization.value === "True") { From d0f0d56620945680869a3fca01d92bfc10ca3323 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:58:12 -0700 Subject: [PATCH 127/148] change enum --- src/registrar/utility/csv_export.py | 2 +- src/registrar/utility/enums.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index d7dc68bb9..6f6b2c744 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -457,7 +457,7 @@ class MemberExport(BaseExport): user=OuterRef("user"), ) ), - then=Value(DefaultEmail.HELP_EMAIL.value), + then=Value(DefaultUserValues.HELP_EMAIL.value), ), default=F("user__email"), output_field=CharField(), diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 14e1e87ee..232c4056f 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -29,7 +29,7 @@ class LogCode(Enum): DEFAULT = 5 -class DefaultEmail(StrEnum): +class DefaultEmail(Enum): """Stores the string values of default emails Overview of emails: @@ -40,7 +40,6 @@ class DefaultEmail(StrEnum): PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov" LEGACY_DEFAULT = "registrar@dotgov.gov" - HELP_EMAIL = "help@get.gov" class DefaultUserValues(StrEnum): @@ -50,7 +49,7 @@ class DefaultUserValues(StrEnum): - SYSTEM: "System" <= Default username - UNRETRIEVED: "Unretrieved" <= Default email state """ - + HELP_EMAIL = "help@get.gov" SYSTEM = "System" UNRETRIEVED = "Unretrieved" From 82e254af973d98e6deb1ad7637a3457f69cb5606 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:15:03 -0700 Subject: [PATCH 128/148] lint --- .../models/user_portfolio_permission.py | 9 +++++++-- .../models/utility/portfolio_helper.py | 2 ++ src/registrar/utility/csv_export.py | 19 ------------------- src/registrar/views/portfolio_members_json.py | 2 -- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 51f3fa3fe..319f15d67 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -2,7 +2,12 @@ from django.db import models from django.forms import ValidationError from registrar.models.user_domain_role import UserDomainRole from registrar.utility.waffle import flag_is_active_for_user -from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices, DomainRequestPermissionDisplay, MemberPermissionDisplay +from registrar.models.utility.portfolio_helper import ( + UserPortfolioPermissionChoices, + UserPortfolioRoleChoices, + DomainRequestPermissionDisplay, + MemberPermissionDisplay, +) from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField @@ -115,7 +120,7 @@ class UserPortfolioPermission(TimeStampedModel): UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS, ] - + if all(perm in all_permissions for perm in all_domain_perms): return DomainRequestPermissionDisplay.VIEWER_REQUESTER elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions: diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 9b661b316..f1a6cec7a 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -51,6 +51,7 @@ class DomainRequestPermissionDisplay(StrEnum): - VIEWER: "Viewer" - NONE: "None" """ + VIEWER_REQUESTER = "Viewer Requester" VIEWER = "Viewer" NONE = "None" @@ -64,6 +65,7 @@ class MemberPermissionDisplay(StrEnum): - VIEWER: "Viewer" - NONE: "None" """ + MANAGER = "Manager" VIEWER = "Viewer" NONE = "None" diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 6f6b2c744..9b36c4d8b 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -263,25 +263,6 @@ class BaseExport(ABC): def get_model_annotation_dict(cls, **kwargs): return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False) - @classmethod - def export_data_to_csv(cls, csv_file, **kwargs): - """ - All domain metadata: - Exports domains of all statuses plus domain managers. - """ - writer = csv.writer(csv_file) - columns = cls.get_columns() - models_dict = cls.get_model_annotation_dict(**kwargs) - - # Write to csv file before the write_csv - cls.write_csv_before(writer, **kwargs) - - # Write the csv file - rows = cls.write_csv(writer, columns, models_dict) - - # Return rows that for easier parsing and testing - return rows - @classmethod def write_csv( cls, diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 232ca2e6c..b5c608eab 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -1,7 +1,6 @@ from django.http import JsonResponse from django.core.paginator import Paginator from django.db.models import Value, F, CharField, TextField, Q, Case, When, OuterRef, Subquery -from django.db.models.expressions import Func from django.db.models.functions import Cast, Coalesce, Concat from django.contrib.postgres.aggregates import ArrayAgg from django.urls import reverse @@ -214,4 +213,3 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): "svg_icon": ("visibility" if view_only else "settings"), } return member_json - From e9338520fda3993b24ce36fe86ad0766218cbe60 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:18:07 -0700 Subject: [PATCH 129/148] fix js bug on main --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/templates/domain_request_requesting_entity.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 2c10edc7a..5188c7312 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2925,7 +2925,7 @@ document.addEventListener("DOMContentLoaded", () => { const selectParent = select?.parentElement; const suborgContainer = document.getElementById("suborganization-container"); const suborgDetailsContainer = document.getElementById("suborganization-container__details"); - const subOrgCreateNewOption = document.getElementById("option-to-add-suborg").value + const subOrgCreateNewOption = document.getElementById("option-to-add-suborg")?.value; // Make sure all crucial page elements exist before proceeding. // This more or less ensures that we are on the Requesting Entity page, and not elsewhere. if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return; diff --git a/src/registrar/templates/domain_request_requesting_entity.html b/src/registrar/templates/domain_request_requesting_entity.html index f58ed0fae..13be025e7 100644 --- a/src/registrar/templates/domain_request_requesting_entity.html +++ b/src/registrar/templates/domain_request_requesting_entity.html @@ -7,7 +7,7 @@ {% endblock %} {% block form_fields %} - +

        Who will use the domain you’re requesting?

        From ac9ea095ed1a04c8354f881c5bf682069ff2e168 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:20:37 -0700 Subject: [PATCH 130/148] lint --- src/registrar/utility/csv_export.py | 4 ++-- src/registrar/utility/enums.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 9b36c4d8b..a03e51de5 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -198,8 +198,8 @@ class BaseExport(ABC): # We can infer that if we're passing in annotations, # we want to grab the result of said annotation. - if computed_fields : - related_table_fields.extend(computed_fields .keys()) + if computed_fields: + related_table_fields.extend(computed_fields.keys()) # Get prexisting fields on the model model_fields = set() diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 232c4056f..47e6da47f 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -49,6 +49,7 @@ class DefaultUserValues(StrEnum): - SYSTEM: "System" <= Default username - UNRETRIEVED: "Unretrieved" <= Default email state """ + HELP_EMAIL = "help@get.gov" SYSTEM = "System" UNRETRIEVED = "Unretrieved" From fe71af74f4b760651dfff9e8dd76ae52e5430aa8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 26 Nov 2024 16:25:15 -0500 Subject: [PATCH 131/148] clean up js check --- src/registrar/assets/src/js/getgov/requesting-entity.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/requesting-entity.js b/src/registrar/assets/src/js/getgov/requesting-entity.js index 10fb969b9..a7d414f73 100644 --- a/src/registrar/assets/src/js/getgov/requesting-entity.js +++ b/src/registrar/assets/src/js/getgov/requesting-entity.js @@ -14,9 +14,7 @@ export function handleRequestingEntityFieldset() { const selectParent = select?.parentElement; const suborgContainer = document.getElementById("suborganization-container"); const suborgDetailsContainer = document.getElementById("suborganization-container__details"); - let subOrgCreateNewOption; - if (subOrgCreateNewOption) - subOrgCreateNewOption = document.getElementById("option-to-add-suborg").value; + const subOrgCreateNewOption = document.getElementById("option-to-add-suborg")?.value; // Make sure all crucial page elements exist before proceeding. // This more or less ensures that we are on the Requesting Entity page, and not elsewhere. if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return; From 14071d00b6c5a104d5e851214a5d719e904a0365 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 26 Nov 2024 14:26:37 -0700 Subject: [PATCH 132/148] icon size change --- src/registrar/assets/sass/_theme/_alerts.scss | 5 +++++ src/registrar/templates/includes/input_with_errors.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/sass/_theme/_alerts.scss index 3cfa768fe..bd3eb9454 100644 --- a/src/registrar/assets/sass/_theme/_alerts.scss +++ b/src/registrar/assets/sass/_theme/_alerts.scss @@ -51,3 +51,8 @@ .usa-site-alert--hot-pink .usa-alert .usa-alert__body::before { background-image: url('../img/usa-icons-bg/error.svg'); } + +.usa-icon-large { + width: 1.5em; + height: 1.5em; +} \ No newline at end of file diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 98328612c..9025ea0b4 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -54,7 +54,7 @@ error messages, if necessary. {% for error in field.errors %}